From b5f85637a77f8d0ecfa6859b0c65bd9a7daf5f97 Mon Sep 17 00:00:00 2001 From: Harvey Kandola Date: Fri, 17 Mar 2017 08:46:33 +0000 Subject: [PATCH] keycloak jwt processing --- app/app/authenticators/keycloak.js | 25 +-- app/app/components/auth-settings.js | 5 +- app/app/pods/auth/keycloak/route.js | 22 +-- app/app/pods/auth/login/controller.js | 23 --- app/app/services/kc-auth.js | 13 +- core/api/endpoint/authentication_endpoint.go | 124 +++++++------- core/api/endpoint/jwt.go | 71 ++++++++ core/api/endpoint/keycloak.go | 169 +++++++++++++++++++ core/api/endpoint/router.go | 3 +- 9 files changed, 334 insertions(+), 121 deletions(-) create mode 100644 core/api/endpoint/keycloak.go diff --git a/app/app/authenticators/keycloak.js b/app/app/authenticators/keycloak.js index 78197cc2..cbf2f00d 100644 --- a/app/app/authenticators/keycloak.js +++ b/app/app/authenticators/keycloak.js @@ -11,7 +11,6 @@ import Ember from 'ember'; import Base from 'ember-simple-auth/authenticators/base'; -import encodingUtil from '../utils/encoding'; import netUtil from '../utils/net'; const { @@ -33,27 +32,17 @@ export default Base.extend({ return reject(); }, - authenticate(credentials) { - let domain = netUtil.getSubdomain(); - let encoded; + authenticate(data) { + data.domain = netUtil.getSubdomain(); - if (typeof credentials === 'object') { - let { password, email } = credentials; - - if (!isPresent(password) || !isPresent(email)) { - return Ember.RSVP.reject("invalid"); - } - - encoded = encodingUtil.Base64.encode(`${domain}:${email}:${password}`); - } else if (typeof credentials === 'string') { - encoded = credentials; - } else { + if (!isPresent(data.token) || !isPresent(data.email)) { return Ember.RSVP.reject("invalid"); } - let headers = { 'Authorization': 'Basic ' + encoded }; - - return this.get('ajax').post('public/authenticate', { headers }); + return this.get('ajax').post('public/authenticate/keycloak', { + data: JSON.stringify(data), + contentType: 'json' + }); }, invalidate() { diff --git a/app/app/components/auth-settings.js b/app/app/components/auth-settings.js index 070bac5b..6c2ae662 100644 --- a/app/app/components/auth-settings.js +++ b/app/app/components/auth-settings.js @@ -11,6 +11,7 @@ import Ember from 'ember'; import constants from '../utils/constants'; +import encoding from '../utils/encoding'; const { computed @@ -46,6 +47,7 @@ export default Ember.Component.extend({ config = {}; } else { config = JSON.parse(config); + config.publicKey = encoding.Base64.decode(config.publicKey); } this.set('keycloakConfig', config); @@ -98,7 +100,8 @@ export default Ember.Component.extend({ this.set('keycloakConfig.publicKey', pk); - config = this.get('keycloakConfig'); + config = Ember.copy(this.get('keycloakConfig')); + Ember.set(config, 'publicKey', encoding.Base64.encode(pk)); break; } diff --git a/app/app/pods/auth/keycloak/route.js b/app/app/pods/auth/keycloak/route.js index ef72e79d..b577eb97 100644 --- a/app/app/pods/auth/keycloak/route.js +++ b/app/app/pods/auth/keycloak/route.js @@ -44,17 +44,19 @@ export default Ember.Route.extend({ this.get('kcAuth').fetchProfile(kc).then((profile) => { let data = this.get('kcAuth').mapProfile(kc, profile); - console.log(kc); - console.log(profile); - console.log(data); + // console.log(kc); + // console.log(profile); + // console.log(data); - // this.get("session").authenticate('authenticator:keycloak', data) - // .then(() => { - // this.transitionTo('folders'); - // }, () => { - // this.transitionTo('auth.login'); - // console.log(">>>>> Documize SSO failure"); - // }); + this.get("session").authenticate('authenticator:keycloak', data).then(() => { + debugger; + this.get('audit').record("logged-in-keycloak"); + this.transitionTo('folders'); + }, (reject) => { + debugger; + console.log(">>>>> Documize Keycloak authentication failure"); + this.transitionTo('auth.login'); + }); }, (err) => { console.log(err); diff --git a/app/app/pods/auth/login/controller.js b/app/app/pods/auth/login/controller.js index 24f68934..9768df7a 100644 --- a/app/app/pods/auth/login/controller.js +++ b/app/app/pods/auth/login/controller.js @@ -43,29 +43,6 @@ export default Ember.Controller.extend({ }).catch(() => { this.set('invalidCredentials', true); }); - - // let authProvider = this.get('appMeta.authProvider'); - // let authConfig = this.get('appMeta.authConfig'); - // switch (authProvider) { - // case constants.AuthProvider.Documize: - // let creds = this.getProperties('email', 'password'); - - // this.get('session').authenticate('authenticator:documize', creds) - // .then((response) => { - // this.get('audit').record("logged-in"); - // this.transitionToRoute('folders'); - // return response; - // }).catch(() => { - // this.set('invalidCredentials', true); - // }); - - // break; - - // case constants.AuthProvider.Keycloak: - // // this.get('session').authenticate('authenticator:keycloak', authConfig); - - // break; - // } } } }); diff --git a/app/app/services/kc-auth.js b/app/app/services/kc-auth.js index 5f2a888a..c1d6a70e 100644 --- a/app/app/services/kc-auth.js +++ b/app/app/services/kc-auth.js @@ -63,13 +63,14 @@ export default Ember.Service.extend({ mapProfile(kc, profile) { return { + domain: '', token: kc.token, - enabled: profile.enabled, - email: profile.email, - username: profile.username, - firstname: profile.firstName, - lastname: profile.lastName, - remoteId: profile.id + remoteId: is.null(profile.id) || is.undefined(profile.id) ? profile.email: profile.id, + email: is.null(profile.email) || is.undefined(profile.email) ? '': profile.email, + username: is.null(profile.username) || is.undefined(profile.username) ? '': profile.username, + firstname: is.null(profile.firstName) || is.undefined(profile.firstName) ? profile.username: profile.firstName, + lastname: is.null(profile.lastName) || is.undefined(profile.lastName) ? profile.username: profile.lastName, + enabled: is.null(profile.enabled) || is.undefined(profile.enabled) ? true: profile.enabled }; } }); diff --git a/core/api/endpoint/authentication_endpoint.go b/core/api/endpoint/authentication_endpoint.go index f11773f0..28f8fe74 100644 --- a/core/api/endpoint/authentication_endpoint.go +++ b/core/api/endpoint/authentication_endpoint.go @@ -24,7 +24,7 @@ import ( "github.com/documize/community/core/api/request" "github.com/documize/community/core/api/util" "github.com/documize/community/core/log" - "github.com/documize/community/core/section/provider" + // "github.com/documize/community/core/section/provider" "github.com/documize/community/core/utility" "github.com/documize/community/core/web" ) @@ -45,15 +45,15 @@ func Authenticate(w http.ResponseWriter, r *http.Request) { // decode what we received data := strings.Replace(authHeader, "Basic ", "", 1) - decodedBytes, err := utility.DecodeBase64([]byte(data)) + decodedBytes, err := utility.DecodeBase64([]byte(data)) if err != nil { writeBadRequestError(w, method, "Unable to decode authentication token") return } + decoded := string(decodedBytes) // check that we have domain:email:password (but allow for : in password field!) - decoded := string(decodedBytes) credentials := strings.SplitN(decoded, ":", 3) if len(credentials) != 3 { @@ -228,65 +228,6 @@ func Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { } } -// ValidateAuthToken checks the auth token and returns the corresponding user. -func ValidateAuthToken(w http.ResponseWriter, r *http.Request) { - - // TODO should this go after token validation? - if s := r.URL.Query().Get("section"); s != "" { - if err := provider.Callback(s, w, r); err != nil { - log.Error("section validation failure", err) - w.WriteHeader(http.StatusUnauthorized) - } - return - } - - method := "ValidateAuthToken" - - context, claims, err := decodeJWT(findJWT(r)) - - if err != nil { - log.Error("token validation", err) - w.WriteHeader(http.StatusUnauthorized) - return - } - - request.SetContext(r, context) - p := request.GetPersister(r) - - org, err := p.GetOrganization(context.OrgID) - - if err != nil { - log.Error("token validation", err) - w.WriteHeader(http.StatusUnauthorized) - return - } - - domain := request.GetSubdomainFromHost(r) - - if org.Domain != domain || claims["domain"] != domain { - log.Error("token validation", err) - w.WriteHeader(http.StatusUnauthorized) - return - } - - user, err := getSecuredUser(p, context.OrgID, context.UserID) - - if err != nil { - log.Error("get user error for token validation", err) - w.WriteHeader(http.StatusUnauthorized) - return - } - - json, err := json.Marshal(user) - - if err != nil { - writeJSONMarshalError(w, method, "user", err) - return - } - - writeSuccessBytes(w, json) -} - // Certain assets/URL do not require authentication. // Just stops the log files being clogged up with failed auth errors. func preAuthorizeStaticAssets(r *http.Request) bool { @@ -303,3 +244,62 @@ func preAuthorizeStaticAssets(r *http.Request) bool { return false } + +// // ValidateAuthToken checks the auth token and returns the corresponding user. +// func ValidateAuthToken(w http.ResponseWriter, r *http.Request) { + +// // TODO should this go after token validation? +// if s := r.URL.Query().Get("section"); s != "" { +// if err := provider.Callback(s, w, r); err != nil { +// log.Error("section validation failure", err) +// w.WriteHeader(http.StatusUnauthorized) +// } +// return +// } + +// method := "ValidateAuthToken" + +// context, claims, err := decodeJWT(findJWT(r)) + +// if err != nil { +// log.Error("token validation", err) +// w.WriteHeader(http.StatusUnauthorized) +// return +// } + +// request.SetContext(r, context) +// p := request.GetPersister(r) + +// org, err := p.GetOrganization(context.OrgID) + +// if err != nil { +// log.Error("token validation", err) +// w.WriteHeader(http.StatusUnauthorized) +// return +// } + +// domain := request.GetSubdomainFromHost(r) + +// if org.Domain != domain || claims["domain"] != domain { +// log.Error("token validation", err) +// w.WriteHeader(http.StatusUnauthorized) +// return +// } + +// user, err := getSecuredUser(p, context.OrgID, context.UserID) + +// if err != nil { +// log.Error("get user error for token validation", err) +// w.WriteHeader(http.StatusUnauthorized) +// return +// } + +// json, err := json.Marshal(user) + +// if err != nil { +// writeJSONMarshalError(w, method, "user", err) +// return +// } + +// writeSuccessBytes(w, json) +// } diff --git a/core/api/endpoint/jwt.go b/core/api/endpoint/jwt.go index 066970aa..41a03daf 100644 --- a/core/api/endpoint/jwt.go +++ b/core/api/endpoint/jwt.go @@ -13,6 +13,7 @@ package endpoint import ( "crypto/rand" + "crypto/rsa" "fmt" "net/http" "strings" @@ -151,3 +152,73 @@ func decodeJWT(tokenString string) (c request.Context, claims map[string]interfa return c, token.Claims, nil } + +// We take in Keycloak token string and decode it. +func decodeKeycloakJWT(t, pk string) (err error) { + // method := "decodeKeycloakJWT" + + log.Info(t) + log.Info(pk) + + var rsaPSSKey *rsa.PublicKey + if rsaPSSKey, err = jwt.ParseRSAPublicKeyFromPEM([]byte(pk)); err != nil { + log.Error("Unable to parse RSA public key", err) + return + } + + parts := strings.Split(t, ".") + m := jwt.GetSigningMethod("RSA256") + + err = m.Verify(strings.Join(parts[0:2], "."), parts[2], rsaPSSKey) + if err != nil { + log.Error("Error while verifying key", err) + return + } + + // token, err := jwt.Parse(t, func(token *jwt.Token) (interface{}, error) { + // p, pe := jwt.ParseRSAPublicKeyFromPEM([]byte(pk)) + // if pe != nil { + // log.Error("have jwt err", pe) + // } + // return p, pe + // // return []byte(jwtKey), nil + // }) + + // if err != nil { + // err = fmt.Errorf("bad authorization token") + // return + // } + + // if !token.Valid { + // if ve, ok := err.(*jwt.ValidationError); ok { + // if ve.Errors&jwt.ValidationErrorMalformed != 0 { + // log.Error("invalid token", err) + // err = fmt.Errorf("bad token") + // return + // } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { + // log.Error("expired token", err) + // err = fmt.Errorf("expired token") + // return + // } else { + // log.Error("invalid token", err) + // err = fmt.Errorf("bad token") + // return + // } + // } else { + // log.Error("invalid token", err) + // err = fmt.Errorf("bad token") + // return + // } + // } + + // email := token.Claims["user"].(string) + // exp := token.Claims["exp"].(string) + // sub := token.Claims["sub"].(string) + + // if len(email) == 0 || len(exp) == 0 || len(sub) == 0 { + // err = fmt.Errorf("%s : unable parse Keycloak token data", method) + // return + // } + + return nil +} diff --git a/core/api/endpoint/keycloak.go b/core/api/endpoint/keycloak.go new file mode 100644 index 00000000..f8f3fbdf --- /dev/null +++ b/core/api/endpoint/keycloak.go @@ -0,0 +1,169 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +package endpoint + +import ( + "database/sql" + "encoding/json" + "io/ioutil" + "net/http" + "strings" + + "github.com/documize/community/core/api/endpoint/models" + "github.com/documize/community/core/api/request" + "github.com/documize/community/core/log" + // "github.com/documize/community/core/section/provider" + "github.com/documize/community/core/utility" +) + +// AuthenticateKeycloak checks Keycloak authentication credentials. +// +// TODO: +// 1. validate keycloak token +// 2. implement new user additions: user & account with RefID +// +func AuthenticateKeycloak(w http.ResponseWriter, r *http.Request) { + method := "AuthenticateKeycloak" + p := request.GetPersister(r) + + defer utility.Close(r.Body) + body, err := ioutil.ReadAll(r.Body) + if err != nil { + writeBadRequestError(w, method, "Bad payload") + return + } + + a := keycloakAuthRequest{} + err = json.Unmarshal(body, &a) + if err != nil { + writePayloadError(w, method, err) + return + } + + // Clean data. + a.Domain = strings.TrimSpace(strings.ToLower(a.Domain)) + a.Domain = request.CheckDomain(a.Domain) // TODO optimize by removing this once js allows empty domains + a.Email = strings.TrimSpace(strings.ToLower(a.Email)) + + // Check for required fields. + if len(a.Email) == 0 { + writeUnauthorizedError(w) + return + } + + // Validate Keycloak credentials + pks := "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1NSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTAwRzI3KzZYNzJFWllIY3NyY1pHekYwZzFsL1gzeVdLS20vZ3NnMCtjMWdXQ2R4ZmI4QmtkbFdCcXhXZVRoSEZCVUVETnorakFyTjBlL0dFMXorMmxnQzJlMkQwemFlcjdlSHZ6bzlBK1hkb0h4KzRNS3RUbkxZZS9aYUFpc3ExSHVURkRKZElKZFRJVUpTWUFXZlNrSmJtdGhIOUVPMmF3SVhEQzlMMWpDa2IwNHZmZ0xERFA3bVo1YzV6NHJPcGluTU45V3RkSm8xeC90VG0xVDlwRHQ3NDRIUHBoMENSbW5OcTRCdWo2SGpaQ3hCcFF1aUp5am0yT0lIdm4vWUJxVUlSUitMcFlJREV1d2FQRU04QkF1eWYvU3BBTGNNaG9oZndzR255QnFMV3QwVFBua3plZjl6ZWN3WEdsQXlYbUZCWlVkR1k3Z0hOdDRpVmdvMXp5d0lEQVFBQi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==" + pkb, err := utility.DecodeBase64([]byte(pks)) + if err != nil { + log.Error("", err) + writeBadRequestError(w, method, "Unable to decode authentication token") + return + } + pk := string(pkb) + pk = ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA00G27+6X72EZYHcsrcZGzF0g1l/X3yWKKm/gsg0+c1gWCdxfb8BkdlWBqxWeThHFBUEDNz+jArN0e/GE1z+2lgC2e2D0zaer7eHvzo9A+XdoHx+4MKtTnLYe/ZaAisq1HuTFDJdIJdTIUJSYAWfSkJbmthH9EO2awIXDC9L1jCkb04vfgLDDP7mZ5c5z4rOpinMN9WtdJo1x/tTm1T9pDt744HPph0CRmnNq4Buj6HjZCxBpQuiJyjm2OIHvn/YBqUIRR+LpYIDEuwaPEM8BAuyf/SpALcMhohfwsGnyBqLWt0TPnkzef9zecwXGlAyXmFBZUdGY7gHNt4iVgo1zywIDAQAB +-----END PUBLIC KEY----- + ` + + err = decodeKeycloakJWT(a.Token, pk) + if err != nil { + writeServerError(w, method, err) + return + } + + log.Info("keycloak logon attempt " + a.Email + " @ " + a.Domain) + + user, err := p.GetUserByDomain(a.Domain, a.Email) + if err != nil && err != sql.ErrNoRows { + writeServerError(w, method, err) + return + } + + // Create user account if not found + if err == sql.ErrNoRows { + log.Info("keycloak add user " + a.Email + " @ " + a.Domain) + + p.Context.Transaction, err = request.Db.Beginx() + if err != nil { + writeTransactionError(w, method, err) + return + } + + } + + // Password correct and active user + if a.Email != strings.TrimSpace(strings.ToLower(user.Email)) { + writeUnauthorizedError(w) + return + } + + org, err := p.GetOrganizationByDomain(a.Domain) + if err != nil { + writeUnauthorizedError(w) + return + } + + // Attach user accounts and work out permissions. + attachUserAccounts(p, org.RefID, &user) + + // No accounts signals data integrity problem + // so we reject login request. + if len(user.Accounts) == 0 { + writeUnauthorizedError(w) + return + } + + // Abort login request if account is disabled. + for _, ac := range user.Accounts { + if ac.OrgID == org.RefID { + if ac.Active == false { + writeUnauthorizedError(w) + return + } + break + } + } + + // Generate JWT token + authModel := models.AuthenticationModel{} + authModel.Token = generateJWT(user.RefID, org.RefID, a.Domain) + authModel.User = user + + json, err := json.Marshal(authModel) + if err != nil { + writeJSONMarshalError(w, method, "user", err) + return + } + + writeSuccessBytes(w, json) +} + +// Data received via Keycloak client library +type keycloakAuthRequest struct { + Domain string `json:"domain"` + Token string `json:"token"` + RemoteID string `json:"remoteId"` + Email string `json:"email"` + Username string `json:"username"` + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Enabled bool `json:"enabled"` +} + +// Keycloak server configuration +type keycloakConfig struct { + URL string `json:"url"` + Realm string `json:"realm"` + ClientID string `json:"clientId"` + PublicKey string `json:"publicKey"` +} diff --git a/core/api/endpoint/router.go b/core/api/endpoint/router.go index 6a92219a..8d02c036 100644 --- a/core/api/endpoint/router.go +++ b/core/api/endpoint/router.go @@ -132,8 +132,9 @@ func init() { //************************************************** log.IfErr(Add(RoutePrefixPublic, "meta", []string{"GET", "OPTIONS"}, nil, GetMeta)) + log.IfErr(Add(RoutePrefixPublic, "authenticate/keycloak", []string{"POST", "OPTIONS"}, nil, AuthenticateKeycloak)) log.IfErr(Add(RoutePrefixPublic, "authenticate", []string{"POST", "OPTIONS"}, nil, Authenticate)) - log.IfErr(Add(RoutePrefixPublic, "validate", []string{"GET", "OPTIONS"}, nil, ValidateAuthToken)) + // log.IfErr(Add(RoutePrefixPublic, "validate", []string{"GET", "OPTIONS"}, nil, ValidateAuthToken)) log.IfErr(Add(RoutePrefixPublic, "forgot", []string{"POST", "OPTIONS"}, nil, ForgotUserPassword)) log.IfErr(Add(RoutePrefixPublic, "reset/{token}", []string{"POST", "OPTIONS"}, nil, ResetUserPassword)) log.IfErr(Add(RoutePrefixPublic, "share/{folderID}", []string{"POST", "OPTIONS"}, nil, AcceptSharedFolder))