mirror of
https://github.com/documize/community.git
synced 2025-07-19 13:19:43 +02:00
improved Keycloak error handling
This commit is contained in:
parent
ce42a18fac
commit
1e077fdf2e
10 changed files with 773 additions and 705 deletions
|
@ -12,12 +12,13 @@
|
|||
import Ember from 'ember';
|
||||
import constants from '../../utils/constants';
|
||||
import encoding from '../../utils/encoding';
|
||||
import NotifierMixin from "../../mixins/notifier";
|
||||
|
||||
const {
|
||||
computed
|
||||
} = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
export default Ember.Component.extend(NotifierMixin, {
|
||||
appMeta: Ember.inject.service(),
|
||||
isDocumizeProvider: computed.equal('authProvider', constants.AuthProvider.Documize),
|
||||
isKeycloakProvider: computed.equal('authProvider', constants.AuthProvider.Keycloak),
|
||||
|
@ -33,7 +34,8 @@ export default Ember.Component.extend({
|
|||
clientId: '',
|
||||
publicKey: '',
|
||||
adminUser: '',
|
||||
adminPassword: ''
|
||||
adminPassword: '',
|
||||
group: ''
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
|
@ -104,16 +106,50 @@ export default Ember.Component.extend({
|
|||
}
|
||||
|
||||
config = Ember.copy(this.get('keycloakConfig'));
|
||||
config.url = config.url.trim();
|
||||
config.realm = config.realm.trim();
|
||||
config.clientId = config.clientId.trim();
|
||||
config.publicKey = config.publicKey.trim();
|
||||
config.group = is.undefined(config.group) ? '' : config.group.trim();
|
||||
config.adminUser = config.adminUser.trim();
|
||||
config.adminPassword = config.adminPassword.trim();
|
||||
|
||||
if (is.endWith(config.url, '/')) {
|
||||
config.url = config.url.substring(0, config.url.length-1);
|
||||
}
|
||||
|
||||
Ember.set(config, 'publicKey', encoding.Base64.encode(this.get('keycloakConfig.publicKey')));
|
||||
break;
|
||||
}
|
||||
|
||||
let data = { authProvider: provider, authConfig: JSON.stringify(config) };
|
||||
|
||||
this.get('onSave')(provider, config).then(() => {
|
||||
this.get('onSave')(data).then(() => {
|
||||
if (data.authProvider === constants.AuthProvider.Keycloak) {
|
||||
this.get('onSync')().then((response) => {
|
||||
if (response.isError) {
|
||||
this.showNotification(response.message);
|
||||
data.authProvider = constants.AuthProvider.Documize;
|
||||
this.get('onSave')(data).then(() => {
|
||||
this.showNotification('Reverted back to Documize');
|
||||
});
|
||||
} else {
|
||||
if (data.authProvider === this.get('appMeta.authProvider')) {
|
||||
this.showNotification(response.message);
|
||||
} else {
|
||||
this.get('onChange')(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.showNotification('Saved');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onSync() {
|
||||
this.get('onSync')();
|
||||
}
|
||||
}
|
||||
});
|
||||
/*
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl4M0UGhKFHe6LKyx2qNu5zTzYifMcsyvH+lV2Z3vgwQtuCf5zFrW/fHglBq9C1DQko/r2eUlVQOM+9C5nfmI60cLVGXviXRU1nWZ3MKQDogaVmSqnESOyVqBfOFEHbjuEeh5xqsLTIGElHFkEVgOfbsqs4GSmCYDgkYc6GMM9YIsk86VbBmprfaXUHmO44cR+Kh6y7rvoTAfKSohRav4+6Pl2+kZRj6SebG629OQb+q6IWVe93kC6NJWk9Y4v5teaAKui/VsoY83Ox/AblNt1wUl4QPrS9t/Be1h0M9XHfmQkmWAZnMkeo6vkcwvU9ioXkX4Zy/148M8u+WXSpgagQIDAQAB
|
||||
|
||||
*/
|
|
@ -32,23 +32,23 @@ export default Ember.Route.extend({
|
|||
resolve();
|
||||
}
|
||||
|
||||
this.get('kcAuth').fetchProfile().then((profile) => {
|
||||
let data = this.get('kcAuth').mapProfile(profile);
|
||||
|
||||
this.get("session").authenticate('authenticator:keycloak', data).then(() => {
|
||||
this.get('audit').record("logged-in-keycloak");
|
||||
this.transitionTo('folders');
|
||||
}, (reject) => {
|
||||
this.set('message', reject.Error);
|
||||
this.set('mode', 'reject');
|
||||
resolve();
|
||||
});
|
||||
this.get('kcAuth').fetchProfile().then((profile) => {
|
||||
let data = this.get('kcAuth').mapProfile(profile);
|
||||
|
||||
this.get("session").authenticate('authenticator:keycloak', data).then(() => {
|
||||
this.get('audit').record("logged-in-keycloak");
|
||||
this.transitionTo('folders');
|
||||
}, (reject) => {
|
||||
this.set('message', reject.Error);
|
||||
this.set('mode', 'reject');
|
||||
this.set('message', reject);
|
||||
resolve();
|
||||
});
|
||||
|
||||
}, (reject) => {
|
||||
this.set('mode', 'reject');
|
||||
this.set('message', reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -11,48 +11,39 @@
|
|||
|
||||
import Ember from 'ember';
|
||||
import NotifierMixin from "../../../mixins/notifier";
|
||||
import constants from '../../../utils/constants';
|
||||
// import constants from '../../../utils/constants';
|
||||
|
||||
export default Ember.Controller.extend(NotifierMixin, {
|
||||
global: Ember.inject.service(),
|
||||
appMeta: Ember.inject.service(),
|
||||
session: Ember.inject.service(),
|
||||
|
||||
handleProviderChange(data) {
|
||||
this.get('session').logout();
|
||||
this.set('appMeta.authProvider', data.authProvider);
|
||||
this.set('appMeta.authConfig', data.authConfig);
|
||||
window.location.href= '/';
|
||||
},
|
||||
|
||||
actions: {
|
||||
onSave(provider, config) {
|
||||
if(this.get('session.isGlobalAdmin')) {
|
||||
let data = { authProvider: provider, authConfig: JSON.stringify(config) };
|
||||
|
||||
return this.get('global').saveAuthConfig(data).then(() => {
|
||||
this.showNotification('Saved');
|
||||
|
||||
if (provider !== this.get('appMeta.authProvider')) {
|
||||
if (provider === constants.AuthProvider.Keycloak) {
|
||||
this.get('global').syncExternalUsers().then(() => {
|
||||
this.handleProviderChange(data);
|
||||
});
|
||||
} else {
|
||||
this.handleProviderChange(data);
|
||||
}
|
||||
} else {
|
||||
this.set('appMeta.authProvider', provider);
|
||||
this.set('appMeta.authConfig', config);
|
||||
}
|
||||
});
|
||||
}
|
||||
onSave(data) {
|
||||
return new Ember.RSVP.Promise((resolve) => {
|
||||
if(!this.get('session.isGlobalAdmin')) {
|
||||
resolve();
|
||||
} else {
|
||||
this.get('global').saveAuthConfig(data).then(() => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onSync() {
|
||||
return this.get('global').syncExternalUsers().then((response) => {
|
||||
this.showNotification(response.message);
|
||||
});
|
||||
return new Ember.RSVP.Promise((resolve) => {
|
||||
this.get('global').syncExternalUsers().then((response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onChange(data) {
|
||||
this.get('session').logout();
|
||||
this.set('appMeta.authProvider', data.authProvider);
|
||||
this.set('appMeta.authConfig', data.authConfig);
|
||||
window.location.href= '/';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
{{customize/auth-settings authProvider=model.authProvider authConfig=model.authConfig onSave=(action 'onSave') onSync=(action 'onSync')}}
|
||||
{{customize/auth-settings authProvider=model.authProvider authConfig=model.authConfig
|
||||
onSave=(action 'onSave') onSync=(action 'onSync') onChange=(action 'onChange')}}
|
||||
|
|
|
@ -81,6 +81,8 @@ export default Ember.Service.extend({
|
|||
method: 'GET'
|
||||
}).then((response) => {
|
||||
return response;
|
||||
}).catch((error) => {
|
||||
return error;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -34,13 +34,13 @@ export default Ember.Service.extend({
|
|||
let keycloak = new Keycloak(JSON.parse(this.get('appMeta.authConfig')));
|
||||
this.set('keycloak', keycloak);
|
||||
|
||||
keycloak.onTokenExpired = function () {
|
||||
keycloak.clearToken();
|
||||
};
|
||||
// keycloak.onTokenExpired = function () {
|
||||
// keycloak.clearToken();
|
||||
// };
|
||||
|
||||
keycloak.onAuthRefreshError = function () {
|
||||
keycloak.clearToken();
|
||||
};
|
||||
// keycloak.onAuthRefreshError = function () {
|
||||
// keycloak.clearToken();
|
||||
// };
|
||||
|
||||
this.get('keycloak').init().success(() => {
|
||||
this.get('audit').record("initialized-keycloak");
|
||||
|
|
|
@ -13,6 +13,7 @@ $sidebar-width: 400px;
|
|||
#sidebar-wrapper {
|
||||
z-index: 888;
|
||||
position: fixed;
|
||||
overflow-x: hidden;
|
||||
left: $sidebar-width;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<form class="">
|
||||
<form class=>
|
||||
<div class="form-header">
|
||||
<div class="title">Authentication</div>
|
||||
<div class="tip">Determine the method for user authentication</div>
|
||||
|
@ -25,19 +25,24 @@
|
|||
<div class="tip">e.g. main</div>
|
||||
{{input id="keycloak-realm" type="text" value=keycloakConfig.realm class=(if keycloakRealmError 'error')}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Keycloak Realm Public Key</label>
|
||||
<div class="tip">Copy the RSA Public Key from Realm Settings → Keys</div>
|
||||
{{textarea id="keycloak-publicKey" type="text" value=keycloakConfig.publicKey rows=7 class=(if KeycloakPublicKeyError 'error')}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Keycloak OIDC Client ID</label>
|
||||
<div class="tip">e.g. account</div>
|
||||
{{input id="keycloak-clientId" type="text" value=keycloakConfig.clientId class=(if KeycloakClientIdError 'error')}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Keycloak Realm Public Key</label>
|
||||
<div class="tip">Copy the RSA public key from Realm Settings → Keys</div>
|
||||
{{textarea id="keycloak-publicKey" type="text" value=keycloakConfig.publicKey rows=7 class=(if KeycloakPublicKeyError 'error')}}
|
||||
<label>Keycloak Group ID (Optional)</label>
|
||||
<div class="tip">If you want to sync users in a particular Group (e.g. 'Documize Users'), provide the Group ID (e.g. 511d8b61-1ec8-45f6-bc8d-5de64d54c9d2)</div>
|
||||
{{input id="keycloak-group" type="text" value=keycloakConfig.group}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Keycloak Username</label>
|
||||
<div class="tip">Used to connect with Keycloak and sync users with Documize</div>
|
||||
<div class="tip">Used to connect with Keycloak and sync users with Documize (create user under Master Realm and assign 'view-users' role against Realm specified above)</div>
|
||||
{{input id="keycloak-admin-user" type="text" value=keycloakConfig.adminUser class=(if KeycloakAdminUserError 'error')}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
|
@ -48,6 +53,4 @@
|
|||
{{/if}}
|
||||
|
||||
<div class="regular-button button-blue" {{action 'onSave'}}>save</div>
|
||||
<div class="button-gap" />
|
||||
<div class="regular-button button-green" {{action 'onSync'}}>sync users</div>
|
||||
</form>
|
||||
|
|
|
@ -89,6 +89,8 @@ func AuthenticateKeycloak(w http.ResponseWriter, r *http.Request) {
|
|||
// Decode and verify Keycloak JWT
|
||||
claims, err := decodeKeycloakJWT(a.Token, pk)
|
||||
if err != nil {
|
||||
log.Info("decodeKeycloakJWT failed")
|
||||
log.Info(pk)
|
||||
util.WriteRequestError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -180,12 +182,14 @@ func SyncKeycloak(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var result struct {
|
||||
Message string `json:"message"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
|
||||
// Org contains raw auth provider config
|
||||
org, err := p.GetOrganization(p.Context.OrgID)
|
||||
if err != nil {
|
||||
result.Message = "Unable to get organization record"
|
||||
result.Message = "Error: unable to get organization record"
|
||||
result.IsError = true
|
||||
log.Error(result.Message, err)
|
||||
util.WriteJSON(w, result)
|
||||
return
|
||||
|
@ -193,7 +197,8 @@ func SyncKeycloak(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Exit if not using Keycloak
|
||||
if org.AuthProvider != "keycloak" {
|
||||
result.Message = "Skipping user sync with Keycloak as it is not the configured option"
|
||||
result.Message = "Error: skipping user sync with Keycloak as it is not the configured option"
|
||||
result.IsError = true
|
||||
log.Info(result.Message)
|
||||
util.WriteJSON(w, result)
|
||||
return
|
||||
|
@ -203,7 +208,8 @@ func SyncKeycloak(w http.ResponseWriter, r *http.Request) {
|
|||
c := keycloakConfig{}
|
||||
err = json.Unmarshal([]byte(org.AuthConfig), &c)
|
||||
if err != nil {
|
||||
result.Message = "Unable process Keycloak public key"
|
||||
result.Message = "Error: unable read Keycloak configuration data"
|
||||
result.IsError = true
|
||||
log.Error(result.Message, err)
|
||||
util.WriteJSON(w, result)
|
||||
return
|
||||
|
@ -212,7 +218,8 @@ func SyncKeycloak(w http.ResponseWriter, r *http.Request) {
|
|||
// User list from Keycloak
|
||||
kcUsers, err := KeycloakUsers(c)
|
||||
if err != nil {
|
||||
result.Message = "Unable to fetch Keycloak users: " + err.Error()
|
||||
result.Message = "Error: unable to fetch Keycloak users: " + err.Error()
|
||||
result.IsError = true
|
||||
log.Error(result.Message, err)
|
||||
util.WriteJSON(w, result)
|
||||
return
|
||||
|
@ -221,7 +228,8 @@ func SyncKeycloak(w http.ResponseWriter, r *http.Request) {
|
|||
// User list from Documize
|
||||
dmzUsers, err := p.GetUsersForOrganization()
|
||||
if err != nil {
|
||||
result.Message = "Unable to fetch Documize users"
|
||||
result.Message = "Error: unable to fetch Documize users"
|
||||
result.IsError = true
|
||||
log.Error(result.Message, err)
|
||||
util.WriteJSON(w, result)
|
||||
return
|
||||
|
@ -339,38 +347,53 @@ func KeycloakUsers(c keycloakConfig) (users []entity.User, err error) {
|
|||
fmt.Sprintf("%s/realms/master/protocol/openid-connect/token", c.URL),
|
||||
bytes.NewBufferString(form.Encode()))
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
|
||||
client := &http.Client{}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Info("Keycloak: cannot connect to auth URL")
|
||||
return users, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
log.Info("Keycloak: cannot read response from auth request")
|
||||
log.Info(string(body))
|
||||
return users, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
if res.StatusCode == http.StatusUnauthorized {
|
||||
return users, errors.New("Check Keycloak username/password")
|
||||
}
|
||||
|
||||
return users, errors.New("Keycloak authentication failed " + res.Status)
|
||||
}
|
||||
|
||||
ka := keycloakAPIAuth{}
|
||||
err = json.Unmarshal(body, &ka)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return users, errors.New("Keycloak authentication failed " + res.Status)
|
||||
url := fmt.Sprintf("%s/admin/realms/%s/users?max=500", c.URL, c.Realm)
|
||||
c.Group = strings.TrimSpace(c.Group)
|
||||
|
||||
if len(c.Group) > 0 {
|
||||
log.Info("Keycloak: filtering by Group members")
|
||||
url = fmt.Sprintf("%s/admin/realms/%s/groups/%s/members?max=500", c.URL, c.Realm, c.Group)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET", fmt.Sprintf("%s/admin/realms/%s/users?max=500", c.URL, c.Realm), nil)
|
||||
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ka.AccessToken))
|
||||
|
||||
client = &http.Client{}
|
||||
res, err = client.Do(req)
|
||||
if err != nil {
|
||||
log.Info("Keycloak: unable to fetch users")
|
||||
return users, err
|
||||
|
||||
}
|
||||
|
@ -378,19 +401,29 @@ func KeycloakUsers(c keycloakConfig) (users []entity.User, err error) {
|
|||
defer res.Body.Close()
|
||||
body, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
log.Info("Keycloak: unable to read user list response")
|
||||
return users, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
if c.Group != "" {
|
||||
return users, errors.New("Keycloak Realm/Client/Group ID not found")
|
||||
}
|
||||
|
||||
return users, errors.New("Keycloak Realm/Client Id not found")
|
||||
}
|
||||
|
||||
return users, errors.New("Keycloak users list call failed " + res.Status)
|
||||
}
|
||||
|
||||
kcUsers := []keycloakUser{}
|
||||
err = json.Unmarshal(body, &kcUsers)
|
||||
if err != nil {
|
||||
log.Info("Keycloak: unable to unmarshal user list response")
|
||||
return users, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return users, errors.New("Keycloak /users call failed " + res.Status)
|
||||
}
|
||||
|
||||
for _, kc := range kcUsers {
|
||||
u := entity.User{}
|
||||
u.Email = kc.Email
|
||||
|
@ -426,6 +459,7 @@ type keycloakConfig struct {
|
|||
PublicKey string `json:"publicKey"`
|
||||
AdminUser string `json:"adminUser"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
// keycloakAPIAuth is returned when authenticating with Keycloak REST API.
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue