From e2bc15c2b28013b61ef65c9f270013fbd063fc7a Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Mon, 10 Dec 2012 17:36:33 -0500 Subject: [PATCH] beginning of client registration refactor to track IETF dynreg spec --- .../oauth2/model/ClientDetailsEntity.java | 77 ++++++++++++------- ...eView.java => ClientRegistrationView.java} | 29 +++++-- .../openid/connect/view/ClientUpdateView.java | 3 + .../ClientDynamicRegistrationEndpoint.java | 72 ++++++++++------- .../resources/db/tables/database_tables.sql | 17 ++-- 5 files changed, 126 insertions(+), 72 deletions(-) rename openid-connect-server/src/main/java/org/mitre/openid/connect/view/{ClientAssociateView.java => ClientRegistrationView.java} (62%) diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java index 94585a38b..8cfd46647 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java @@ -63,7 +63,6 @@ public class ClientDetailsEntity implements ClientDetails { /** Our own fields **/ private String clientDescription = ""; // human-readable description - private boolean allowRefresh = false; // do we allow refresh tokens for this client? private boolean allowMultipleAccessTokens = false; // do we allow multiple access tokens, or not? private boolean reuseRefreshToken = false; // do we let someone reuse a refresh token? private boolean dynamicallyRegistered = false; // was this client dynamically registered? @@ -83,7 +82,7 @@ public class ClientDetailsEntity implements ClientDetails { /** Fields from Client Registration Specification **/ private AppType applicationType; - private String applicationName; + private String clientName; private AuthType tokenEndpointAuthType = AuthType.SECRET_BASIC; private UserIdType userIdType; @@ -91,6 +90,8 @@ public class ClientDetailsEntity implements ClientDetails { private String logoUrl; private String policyUrl; + private String clientUrl; + private String tosUrl; private String jwkUrl; private String jwkEncryptionUrl; private String x509Url; @@ -239,19 +240,11 @@ public class ClientDetailsEntity implements ClientDetails { /** * @return the allowRefresh */ - @Basic - @Column(name="allow_refresh") + @Transient public boolean isAllowRefresh() { - return allowRefresh; + return getAuthorizedGrantTypes().contains("refresh_token"); } - /** - * @param allowRefresh Whether to allow for issuance of refresh tokens or not (defaults to false) - */ - public void setAllowRefresh(boolean allowRefresh) { - this.allowRefresh = allowRefresh; - } - @Basic @Column(name="allow_multiple_access_tokens") public boolean isAllowMultipleAccessTokens() { @@ -517,13 +510,13 @@ public class ClientDetailsEntity implements ClientDetails { } @Basic - @Column(name="application_name") - public String getApplicationName() { - return applicationName; + @Column(name="client_name") + public String getClientName() { + return clientName; } - public void setApplicationName(String applicationName) { - this.applicationName = applicationName; + public void setClientName(String clientName) { + this.clientName = clientName; } @Enumerated(EnumType.STRING) @@ -580,6 +573,38 @@ public class ClientDetailsEntity implements ClientDetails { this.policyUrl = policyUrl; } + /** + * @return the clientUrl + */ + @Basic + @Column(name="client_url") + public String getClientUrl() { + return clientUrl; + } + + /** + * @param clientUrl the clientUrl to set + */ + public void setClientUrl(String clientUrl) { + this.clientUrl = clientUrl; + } + + /** + * @return the tosUrl + */ + @Basic + @Column(name="tos_url") + public String getTosUrl() { + return tosUrl; + } + + /** + * @param tosUrl the tosUrl to set + */ + public void setTosUrl(String tosUrl) { + this.tosUrl = tosUrl; + } + @Basic @Column(name="jwk_url") public String getJwkUrl() { @@ -766,8 +791,6 @@ public class ClientDetailsEntity implements ClientDetails { + (id != null ? "id=" + id + ", " : "") + (clientDescription != null ? "clientDescription=" + clientDescription + ", " : "") - + "allowRefresh=" - + allowRefresh + ", allowMultipleAccessTokens=" + allowMultipleAccessTokens + ", reuseRefreshToken=" @@ -799,8 +822,8 @@ public class ClientDetailsEntity implements ClientDetails { + additionalInformation + ", " : "") + (applicationType != null ? "applicationType=" + applicationType + ", " : "") - + (applicationName != null ? "applicationName=" - + applicationName + ", " : "") + + (clientName != null ? "applicationName=" + + clientName + ", " : "") + (tokenEndpointAuthType != null ? "tokenEndpointAuthType=" + tokenEndpointAuthType + ", " : "") + (userIdType != null ? "userIdType=" + userIdType + ", " : "") @@ -864,9 +887,8 @@ public class ClientDetailsEntity implements ClientDetails { + ((additionalInformation == null) ? 0 : additionalInformation .hashCode()); result = prime * result + (allowMultipleAccessTokens ? 1231 : 1237); - result = prime * result + (allowRefresh ? 1231 : 1237); result = prime * result - + ((applicationName == null) ? 0 : applicationName.hashCode()); + + ((clientName == null) ? 0 : clientName.hashCode()); result = prime * result + ((applicationType == null) ? 0 : applicationType.hashCode()); result = prime * result @@ -1002,14 +1024,11 @@ public class ClientDetailsEntity implements ClientDetails { if (allowMultipleAccessTokens != other.allowMultipleAccessTokens) { return false; } - if (allowRefresh != other.allowRefresh) { - return false; - } - if (applicationName == null) { - if (other.applicationName != null) { + if (clientName == null) { + if (other.clientName != null) { return false; } - } else if (!applicationName.equals(other.applicationName)) { + } else if (!clientName.equals(other.clientName)) { return false; } if (applicationType != other.applicationType) { diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientAssociateView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientRegistrationView.java similarity index 62% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientAssociateView.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientRegistrationView.java index 7d215d192..469e9be52 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientAssociateView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientRegistrationView.java @@ -20,11 +20,15 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; /** + * + * Provides a minimal representation of a client's registration information, to be shown from the dynamic registration endpoint + * on the client_register and rotate_secret operations. + * * @author jricher * */ -@Component("clientAssociate") -public class ClientAssociateView extends AbstractView { +@Component("clientRegistration") +public class ClientRegistrationView extends AbstractView { /* (non-Javadoc) * @see org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel(java.util.Map, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) @@ -40,17 +44,26 @@ public class ClientAssociateView extends AbstractView { ClientDetailsEntity client = (ClientDetailsEntity) model.get("client"); OAuth2AccessTokenEntity token = (OAuth2AccessTokenEntity) model.get("token"); - + Boolean fullClient = (Boolean) model.get("fullClient"); // do we display the full client or not? JsonObject obj = new JsonObject(); obj.addProperty("client_id", client.getClientId()); if (client.isSecretRequired()) { obj.addProperty("client_secret", client.getClientSecret()); } - obj.addProperty("registration_access_token", token.getValue()); - if (token.getExpiration() != null) { - obj.addProperty("expires_at", token.getExpiration().getTime()); // TODO: make sure this makes sense? - } else { - obj.addProperty("expires_at", 0); // TODO: configure expiring client secrets. For now, they don't expire + + if (fullClient) { + // TODO: display the rest of the client fields, for now just this to mark changes + obj.addProperty("client_name", client.getClientName()); + } + + + if (token != null) { + obj.addProperty("registration_access_token", token.getValue()); + if (token.getExpiration() != null) { + obj.addProperty("expires_at", token.getExpiration().getTime()); // TODO: make sure this makes sense? + } else { + obj.addProperty("expires_at", 0); // TODO: configure expiring client secrets. For now, they don't expire + } } Writer out = response.getWriter(); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientUpdateView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientUpdateView.java index 3885ce204..51b808376 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientUpdateView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientUpdateView.java @@ -19,6 +19,9 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; /** + * + * Provides a minimal display response for a dynamic client registration's client_update operation. + * * @author jricher * */ diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java index 970765a1d..a6fa57a32 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java @@ -183,23 +183,29 @@ public class ClientDynamicRegistrationEndpoint { }); } - @RequestMapping(params = "type=client_associate", produces = "application/json") - public String clientAssociate( - @RequestParam(value = "contacts", required = false) Set contacts, - @RequestParam(value = "application_type", required = false) AppType applicationType, - @RequestParam(value = "application_name", required = false) String applicationName, + @RequestMapping(params = "operation=client_register", produces = "application/json") + public String clientRegister( + @RequestParam(value = "redirect_uris", required = true) Set redirectUris, + @RequestParam(value = "client_name", required = false) String clientName, + @RequestParam(value = "client_url", required = false) String clientUrl, @RequestParam(value = "logo_url", required = false) String logoUrl, - @RequestParam(value = "redirect_uris", required = false) Set redirectUris, + @RequestParam(value = "contacts", required = false) Set contacts, + @RequestParam(value = "tos_url", required = false) String tosUrl, @RequestParam(value = "token_endpoint_auth_type", required = false) AuthType tokenEndpointAuthType, @RequestParam(value = "policy_url", required = false) String policyUrl, + @RequestParam(value = "jwk_url", required = false) String jwkUrl, @RequestParam(value = "jwk_encryption_url", required = false) String jwkEncryptionUrl, @RequestParam(value = "x509_url", required = false) String x509Url, @RequestParam(value = "x509_encryption_url", required = false) String x509EncryptionUrl, + @RequestParam(value = "default_max_age", required = false) Integer defaultMaxAge, + @RequestParam(value = "default_acr", required = false) String defaultAcr, + + // OPENID CONNECT EXTENSIONS BELOW + @RequestParam(value = "application_type", required = false) AppType applicationType, @RequestParam(value = "sector_identifier_url", required = false) String sectorIdentifierUrl, @RequestParam(value = "user_id_type", required = false) UserIdType userIdType, @RequestParam(value = "require_signed_request_object", required = false) JwsAlgorithm requireSignedRequestObject, - // TODO: JWE needs to be handled properly, see @InitBinder above -- we'll ignore these right now /* @RequestParam(value = "userinfo_signed_response_alg", required = false) String userinfoSignedResponseAlg, @@ -212,9 +218,7 @@ public class ClientDynamicRegistrationEndpoint { @RequestParam(value = "idtoken_encrypted_response_int", required = false) String idtokenEncryptedResponseInt, */ - @RequestParam(value = "default_max_age", required = false) Integer defaultMaxAge, @RequestParam(value = "require_auth_time", required = false, defaultValue = "true") Boolean requireAuthTime, - @RequestParam(value = "default_acr", required = false) String defaultAcr, ModelMap model ) { @@ -223,12 +227,16 @@ public class ClientDynamicRegistrationEndpoint { ClientDetailsEntity client = new ClientDetailsEntity(); - // always generate a secret for this client - client = clientService.generateClientSecret(client); + // if it's not using a private key or no auth, then generate a secret + if (tokenEndpointAuthType != AuthType.PRIVATE_KEY && tokenEndpointAuthType != AuthType.NONE) { + client = clientService.generateClientSecret(client); + } client.setContacts(contacts); client.setApplicationType(applicationType); - client.setApplicationName(applicationName); + client.setClientName(clientName); + client.setClientUrl(clientUrl); + client.setTosUrl(tosUrl); client.setLogoUrl(logoUrl); client.setRegisteredRedirectUri(redirectUris); client.setTokenEndpointAuthType(tokenEndpointAuthType); @@ -247,11 +255,10 @@ public class ClientDynamicRegistrationEndpoint { // defaults for SECOAUTH functionality // TODO: extensions to request, or configuration? client.setScope(Sets.newHashSet("openid", "phone", "address", "profile", "email")); // provision all scopes - client.setAllowRefresh(true); // by default allow refresh tokens on dynamic clients client.setAccessTokenValiditySeconds(3600); // access tokens good for 1hr client.setIdTokenValiditySeconds(600); // id tokens good for 10min client.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked - client.setAuthorizedGrantTypes(Sets.newHashSet("authorization_code")); + client.setAuthorizedGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authoirzation code and refresh token grant types client.setDynamicallyRegistered(true); @@ -259,10 +266,11 @@ public class ClientDynamicRegistrationEndpoint { OAuth2AccessTokenEntity registrationAccessToken = createRegistrationAccessToken(client); + model.put("fullClient", Boolean.TRUE); model.put("client", saved); model.put("token", registrationAccessToken); - return "clientAssociate"; + return "clientRegistration"; } /** @@ -282,7 +290,7 @@ public class ClientDynamicRegistrationEndpoint { } @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('registration-token')") - @RequestMapping(params = "type=rotate_secret", produces = "application/json") + @RequestMapping(params = "operation=rotate_secret", produces = "application/json") public String rotateSecret(OAuth2Authentication auth, ModelMap model) { @@ -316,30 +324,38 @@ public class ClientDynamicRegistrationEndpoint { // save the client ClientDetailsEntity saved = clientService.updateClient(client, client); + model.put("fullClient", Boolean.FALSE); model.put("client", saved); model.put("token", registrationAccessToken); - return "clientAssociate"; + return "clientRegistration"; } @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('registration-token')") - @RequestMapping(params = "type=client_update", produces = "application/json") + @RequestMapping(params = "operation=client_update", produces = "application/json") public String clientUpdate( - @RequestParam(value = "contacts", required = false) Set contacts, - @RequestParam(value = "application_type", required = false) AppType applicationType, - @RequestParam(value = "application_name", required = false) String applicationName, + @RequestParam(value = "redirect_uris", required = true) Set redirectUris, + @RequestParam(value = "client_name", required = false) String clientName, + @RequestParam(value = "client_url", required = false) String clientUrl, @RequestParam(value = "logo_url", required = false) String logoUrl, - @RequestParam(value = "redirect_uris", required = false) Set redirectUris, + @RequestParam(value = "contacts", required = false) Set contacts, + @RequestParam(value = "tos_url", required = false) String tosUrl, @RequestParam(value = "token_endpoint_auth_type", required = false) AuthType tokenEndpointAuthType, @RequestParam(value = "policy_url", required = false) String policyUrl, + @RequestParam(value = "jwk_url", required = false) String jwkUrl, @RequestParam(value = "jwk_encryption_url", required = false) String jwkEncryptionUrl, @RequestParam(value = "x509_url", required = false) String x509Url, @RequestParam(value = "x509_encryption_url", required = false) String x509EncryptionUrl, + @RequestParam(value = "default_max_age", required = false) Integer defaultMaxAge, + @RequestParam(value = "default_acr", required = false) String defaultAcr, + + // OPENID CONNECT EXTENSIONS BELOW + @RequestParam(value = "application_type", required = false) AppType applicationType, @RequestParam(value = "sector_identifier_url", required = false) String sectorIdentifierUrl, @RequestParam(value = "user_id_type", required = false) UserIdType userIdType, @RequestParam(value = "require_signed_request_object", required = false) JwsAlgorithm requireSignedRequestObject, - + @RequestParam(value = "require_auth_time", required = false, defaultValue = "true") Boolean requireAuthTime, // TODO: JWE needs to be handled properly, see @InitBinder above -- we'll ignore these right now /* @RequestParam(value = "userinfo_signed_response_alg", required = false) String userinfoSignedResponseAlg, @@ -352,9 +368,6 @@ public class ClientDynamicRegistrationEndpoint { @RequestParam(value = "idtoken_encrypted_response_int", required = false) String idtokenEncryptedResponseInt, */ - @RequestParam(value = "default_max_age", required = false) Integer defaultMaxAge, - @RequestParam(value = "require_auth_time", required = false) Boolean requireAuthTime, - @RequestParam(value = "default_acr", required = false) String defaultAcr, OAuth2Authentication auth, ModelMap model @@ -369,7 +382,9 @@ public class ClientDynamicRegistrationEndpoint { client.setContacts(contacts); client.setApplicationType(applicationType); - client.setApplicationName(applicationName); + client.setClientName(clientName); + client.setClientUrl(clientUrl); + client.setTosUrl(tosUrl); client.setLogoUrl(logoUrl); client.setRegisteredRedirectUri(redirectUris); client.setTokenEndpointAuthType(tokenEndpointAuthType); @@ -387,8 +402,9 @@ public class ClientDynamicRegistrationEndpoint { ClientDetailsEntity saved = clientService.updateClient(client, client); + model.put("fullClient", Boolean.TRUE); model.put("client", saved); - return "clientUpdate"; + return "clientRegister"; } } diff --git a/openid-connect-server/src/main/resources/db/tables/database_tables.sql b/openid-connect-server/src/main/resources/db/tables/database_tables.sql index f432c4182..7839c8169 100644 --- a/openid-connect-server/src/main/resources/db/tables/database_tables.sql +++ b/openid-connect-server/src/main/resources/db/tables/database_tables.sql @@ -67,11 +67,11 @@ CREATE TABLE IF NOT EXISTS blacklisted_site ( CREATE TABLE IF NOT EXISTS client_details ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, - client_description VARCHAR(256), - allow_refresh BOOLEAN, - allow_multiple_access_tokens BOOLEAN, - reuse_refresh_tokens BOOLEAN, - dynamically_registered BOOLEAN, + + client_description VARCHAR(1024), + allow_multiple_access_tokens BOOLEAN NOT NULL DEFAULT true, + reuse_refresh_tokens BOOLEAN NOT NULL DEFAULT true, + dynamically_registered BOOLEAN NOT NULL DEFAULT false, id_token_validity_seconds BIGINT, client_id VARCHAR(256), @@ -80,12 +80,15 @@ CREATE TABLE IF NOT EXISTS client_details ( refresh_token_validity_seconds BIGINT, application_type VARCHAR(256), - application_name VARCHAR(256), + client_name VARCHAR(256), token_endpoint_auth_type VARCHAR(256), user_id_type VARCHAR(256), logo_url VARCHAR(2048), policy_url VARCHAR(2048), + client_url VARCHAR(2048), + tos_url VARCHAR(2048), + jwk_url VARCHAR(2048), jwk_encryption_url VARCHAR(2048), x509_url VARCHAR(2048), @@ -105,7 +108,7 @@ CREATE TABLE IF NOT EXISTS client_details ( id_token_encrypted_response_int VARCHAR(256), default_max_age BIGINT, - require_auth_time BOOLEAN, + require_auth_time BOOLEAN NOT NULL DEFAULT FALSE, default_acr VARCHAR(256) );