Skip to content

Commit

Permalink
KEYCLOAK-1749 Rotate registration access token, add registration acce…
Browse files Browse the repository at this point in the history
…ss token to admin console
  • Loading branch information
stianst committed Nov 17, 2015
1 parent bad0a95 commit 62c5bc0
Show file tree
Hide file tree
Showing 20 changed files with 187 additions and 50 deletions.
Expand Up @@ -55,9 +55,10 @@ public AdapterConfig getAdapterConfig(String clientId) throws ClientRegistration
return resultStream != null ? deserialize(resultStream, AdapterConfig.class) : null; return resultStream != null ? deserialize(resultStream, AdapterConfig.class) : null;
} }


public void update(ClientRepresentation client) throws ClientRegistrationException { public ClientRepresentation update(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client); String content = serialize(client);
httpUtil.doPut(content, DEFAULT, client.getClientId()); InputStream resultStream = httpUtil.doPut(content, DEFAULT, client.getClientId());
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
} }


public void delete(ClientRepresentation client) throws ClientRegistrationException { public void delete(ClientRepresentation client) throws ClientRegistrationException {
Expand Down
Expand Up @@ -80,15 +80,17 @@ InputStream doGet(String... path) throws ClientRegistrationException {
responseStream.close(); responseStream.close();
return null; return null;
} else { } else {
responseStream.close(); if (responseStream != null) {
responseStream.close();
}
throw new HttpErrorException(response.getStatusLine()); throw new HttpErrorException(response.getStatusLine());
} }
} catch (IOException e) { } catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e); throw new ClientRegistrationException("Failed to send request", e);
} }
} }


void doPut(String content, String... path) throws ClientRegistrationException { InputStream doPut(String content, String... path) throws ClientRegistrationException {
try { try {
HttpPut request = new HttpPut(getUrl(baseUri, path)); HttpPut request = new HttpPut(getUrl(baseUri, path));


Expand All @@ -100,10 +102,20 @@ void doPut(String content, String... path) throws ClientRegistrationException {


HttpResponse response = httpClient.execute(request); HttpResponse response = httpClient.execute(request);
if (response.getEntity() != null) { if (response.getEntity() != null) {
response.getEntity().getContent().close(); response.getEntity().getContent();
} }


if (response.getStatusLine().getStatusCode() != 200) { InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}

if (response.getStatusLine().getStatusCode() == 200) {
return responseStream;
} else {
if (responseStream != null) {
responseStream.close();
}
throw new HttpErrorException(response.getStatusLine()); throw new HttpErrorException(response.getStatusLine());
} }
} catch (IOException e) { } catch (IOException e) {
Expand Down
Expand Up @@ -270,7 +270,10 @@ client-certificate-import=Client Certificate Import
import-client-certificate=Import Client Certificate import-client-certificate=Import Client Certificate
jwt-import.key-alias.tooltip=Archive alias for your certificate. jwt-import.key-alias.tooltip=Archive alias for your certificate.
secret=Secret secret=Secret
regenerate-secret=Regenerate Secret regenerate-secret=Regenerate Secretsecret=Secret
registrationAccessToken=Registration access token
registrationAccessToken.regenerate=Regenerate registration access token
registrationAccessToken.tooltip=The registration access token provides access for clients to the client registration service.
add-role=Add Role add-role=Add Role
role-name=Role Name role-name=Role Name
composite=Composite composite=Composite
Expand Down
Expand Up @@ -30,7 +30,7 @@ module.controller('ClientRoleListCtrl', function($scope, $location, realm, clien
}); });
}); });


module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, clientAuthenticatorProviders, clientConfigProperties, Client) { module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, clientAuthenticatorProviders, clientConfigProperties, Client, ClientRegistrationAccessToken, Notifications) {
$scope.realm = realm; $scope.realm = realm;
$scope.client = angular.copy(client); $scope.client = angular.copy(client);
$scope.clientAuthenticatorProviders = clientAuthenticatorProviders; $scope.clientAuthenticatorProviders = clientAuthenticatorProviders;
Expand Down Expand Up @@ -68,6 +68,17 @@ module.controller('ClientCredentialsCtrl', function($scope, $location, realm, cl
} }
}, true); }, true);


$scope.regenerateRegistrationAccessToken = function() {
var secret = ClientRegistrationAccessToken.update({ realm : $scope.realm.realm, client : $scope.client.id },
function(data) {
Notifications.success('The registration access token has been updated.');
$scope.client['registrationAccessToken'] = data.registrationAccessToken;
},
function() {
Notifications.error('Failed to update the registration access token');
}
);
};
}); });


module.controller('ClientSecretCtrl', function($scope, $location, ClientSecret, Notifications) { module.controller('ClientSecretCtrl', function($scope, $location, ClientSecret, Notifications) {
Expand Down
Expand Up @@ -981,6 +981,17 @@ module.factory('ClientSecret', function($resource) {
}); });
}); });


module.factory('ClientRegistrationAccessToken', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients/:client/registration-access-token', {
realm : '@realm',
client : '@client'
}, {
update : {
method : 'POST'
}
});
});

module.factory('ClientOrigins', function($resource) { module.factory('ClientOrigins', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients/:client/allowed-origins', { return $resource(authUrl + '/admin/realms/:realm/clients/:client/allowed-origins', {
realm : '@realm', realm : '@realm',
Expand Down
@@ -1,5 +1,5 @@
<div> <div>
<form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients" data-ng-show="currentAuthenticatorConfigProperties.length > 0" data-ng-controller="ClientGenericCredentialsCtrl"> <form class="form-horizontal no-margin-top" name="credentialForm" novalidate kc-read-only="!access.manageClients" data-ng-show="currentAuthenticatorConfigProperties.length > 0" data-ng-controller="ClientGenericCredentialsCtrl">
<fieldset> <fieldset>
<kc-provider-config realm="realm" config="client.attributes" properties="currentAuthenticatorConfigProperties"></kc-provider-config> <kc-provider-config realm="realm" config="client.attributes" properties="currentAuthenticatorConfigProperties"></kc-provider-config>
</fieldset> </fieldset>
Expand Down
@@ -1,5 +1,5 @@
<div> <div>
<form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSignedJWTCtrl"> <form class="form-horizontal no-margin-top" name="keyForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSignedJWTCtrl">
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label> <label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
<kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
Expand Down
@@ -1,5 +1,5 @@
<div> <div>
<form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSecretCtrl"> <form class="form-horizontal no-margin-top" name="credentialForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSecretCtrl">
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="secret">{{:: 'secret' | translate}}</label> <label class="col-md-2 control-label" for="secret">{{:: 'secret' | translate}}</label>
<div class="col-sm-6"> <div class="col-sm-6">
Expand Down
Expand Up @@ -28,6 +28,11 @@
<div data-ng-include="resourceUrl + '/partials/' + clientAuthenticatorConfigPartial"> <div data-ng-include="resourceUrl + '/partials/' + clientAuthenticatorConfigPartial">
</div> </div>


<hr/>

<div data-ng-include="resourceUrl + '/partials/client-registration-access-token.html'">
</div>

</div> </div>


<kc-menu></kc-menu> <kc-menu></kc-menu>
@@ -0,0 +1,18 @@
<div>
<form class="form-horizontal" name="registrationAccessTokenForm" novalidate kc-read-only="!access.manageClients">
<div class="form-group">
<label class="col-md-2 control-label" for="registrationAccessToken">{{:: 'registrationAccessToken' | translate}}</label>
<div class="col-sm-6">
<div class="row">
<div class="col-sm-6">
<input readonly kc-select-action="click" class="form-control" type="text" id="registrationAccessToken" name="registrationAccessToken" data-ng-model="client.registrationAccessToken">
</div>
<div class="col-sm-6" data-ng-show="access.manageClients">
<button type="submit" data-ng-click="regenerateRegistrationAccessToken()" class="btn btn-default">{{:: 'registrationAccessToken.regenerate' | translate}}</button>
</div>
</div>
</div>
<kc-tooltip>{{:: 'registrationAccessToken.tooltip' | translate}}</kc-tooltip>
</div>
</form>
</div>
Expand Up @@ -22,6 +22,11 @@ table {
margin-top: 20px; margin-top: 20px;
} }


.no-margin-top {
margin-top: 0px !important;
}




/*********** Loading ***********/ /*********** Loading ***********/


Expand Down
@@ -1,6 +1,7 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;


import org.bouncycastle.openssl.PEMWriter; import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.common.util.Base64Url;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
Expand All @@ -26,12 +27,7 @@
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.security.Key; import java.security.*;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
Expand All @@ -47,6 +43,8 @@
*/ */
public final class KeycloakModelUtils { public final class KeycloakModelUtils {


private static final int RANDOM_PASSWORD_BYTES = 32;

private KeycloakModelUtils() { private KeycloakModelUtils() {
} }


Expand Down Expand Up @@ -178,12 +176,22 @@ public static CertificateRepresentation generateKeyPairCertificate(String subjec
return rep; return rep;
} }


public static UserCredentialModel generateSecret(ClientModel app) { public static UserCredentialModel generateSecret(ClientModel client) {
UserCredentialModel secret = UserCredentialModel.generateSecret(); UserCredentialModel secret = UserCredentialModel.generateSecret();
app.setSecret(secret.getValue()); client.setSecret(secret.getValue());
return secret; return secret;
} }


public static void generateRegistrationAccessToken(ClientModel client) {
client.setRegistrationSecret(generatePassword());
}

public static String generatePassword() {
byte[] buf = new byte[RANDOM_PASSWORD_BYTES];
new SecureRandom().nextBytes(buf);
return Base64Url.encode(buf);
}

public static String getDefaultClientAuthenticatorType() { public static String getDefaultClientAuthenticatorType() {
return "client-secret"; return "client-secret";
} }
Expand Down
Expand Up @@ -24,6 +24,7 @@ public class ClientRegAuth {
private AccessToken.Access bearerRealmAccess; private AccessToken.Access bearerRealmAccess;


private boolean authenticated = false; private boolean authenticated = false;
private boolean registrationAccessToken = false;


public ClientRegAuth(KeycloakSession session, EventBuilder event) { public ClientRegAuth(KeycloakSession session, EventBuilder event) {
this.session = session; this.session = session;
Expand All @@ -48,6 +49,7 @@ private void init() {
if (split[1].indexOf('.') == -1) { if (split[1].indexOf('.') == -1) {
token = split[1]; token = split[1];
authenticated = true; authenticated = true;
registrationAccessToken = true;
} else { } else {
AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session, realm); AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session, realm);
bearerRealmAccess = authResult.getToken().getResourceAccess(Constants.REALM_MANAGEMENT_CLIENT_ID); bearerRealmAccess = authResult.getToken().getResourceAccess(Constants.REALM_MANAGEMENT_CLIENT_ID);
Expand All @@ -59,6 +61,10 @@ public boolean isAuthenticated() {
return authenticated; return authenticated;
} }


public boolean isRegistrationAccessToken() {
return registrationAccessToken;
}

public void requireCreate() { public void requireCreate() {
if (!authenticated) { if (!authenticated) {
event.error(Errors.NOT_ALLOWED); event.error(Errors.NOT_ALLOWED);
Expand Down
@@ -1,5 +1,6 @@
package org.keycloak.services.clientregistration; package org.keycloak.services.clientregistration;


import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
Expand Down Expand Up @@ -28,6 +29,11 @@ public Object getProvider(@PathParam("provider") String providerId) {
checkSsl(); checkSsl();


ClientRegistrationProvider provider = session.getProvider(ClientRegistrationProvider.class, providerId); ClientRegistrationProvider provider = session.getProvider(ClientRegistrationProvider.class, providerId);

if (provider == null) {
throw new NotFoundException("Client registration provider not found");
}

provider.setEvent(event); provider.setEvent(event);
provider.setAuth(new ClientRegAuth(session, event)); provider.setAuth(new ClientRegAuth(session, event));
return provider; return provider;
Expand Down
Expand Up @@ -5,6 +5,7 @@
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
Expand Down Expand Up @@ -38,7 +39,7 @@ public Response create(ClientRepresentation client) {


try { try {
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true); ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
clientModel.setRegistrationSecret(TokenGenerator.createRegistrationAccessToken()); KeycloakModelUtils.generateRegistrationAccessToken(clientModel);


client = ModelToRepresentation.toRepresentation(clientModel); client = ModelToRepresentation.toRepresentation(clientModel);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build(); URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
Expand All @@ -59,6 +60,10 @@ public Response get(@PathParam("clientId") String clientId) {
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId); ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
auth.requireView(client); auth.requireView(client);


if (auth.isRegistrationAccessToken()) {
KeycloakModelUtils.generateRegistrationAccessToken(client);
}

event.client(client.getClientId()).success(); event.client(client.getClientId()).success();
return Response.ok(ModelToRepresentation.toRepresentation(client)).build(); return Response.ok(ModelToRepresentation.toRepresentation(client)).build();
} }
Expand All @@ -74,8 +79,14 @@ public Response update(@PathParam("clientId") String clientId, ClientRepresentat


RepresentationToModel.updateClient(rep, client); RepresentationToModel.updateClient(rep, client);


if (auth.isRegistrationAccessToken()) {
KeycloakModelUtils.generateRegistrationAccessToken(client);
}

rep = ModelToRepresentation.toRepresentation(client);

event.client(client.getClientId()).success(); event.client(client.getClientId()).success();
return Response.status(Response.Status.OK).build(); return Response.ok(rep).build();
} }


@DELETE @DELETE
Expand Down

This file was deleted.

Expand Up @@ -15,7 +15,6 @@
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.clientregistration.ClientRegAuth; import org.keycloak.services.clientregistration.ClientRegAuth;
import org.keycloak.services.clientregistration.ClientRegistrationProvider; import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import org.keycloak.services.clientregistration.TokenGenerator;


import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.POST; import javax.ws.rs.POST;
Expand Down
Expand Up @@ -214,6 +214,24 @@ public CredentialRepresentation regenerateSecret() {
return rep; return rep;
} }


/**
* Generate a new registration access token for the client
*
* @return
*/
@Path("registration-access-token")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public ClientRepresentation regenerateRegistrationAccessToken() {
auth.requireManage();

KeycloakModelUtils.generateRegistrationAccessToken(client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(rep).success();
return rep;
}

/** /**
* Get the client secret * Get the client secret
* *
Expand Down

0 comments on commit 62c5bc0

Please sign in to comment.