Move dyn client validation to dedicated service
Which allows easier overriding for integrationspull/1611/head
parent
68b2cc6a8c
commit
846727e9b3
|
@ -22,7 +22,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>openid-connect-parent</artifactId>
|
<artifactId>openid-connect-parent</artifactId>
|
||||||
<groupId>org.mitre</groupId>
|
<groupId>org.mitre</groupId>
|
||||||
<version>1.3.5.cnaf.v20190827</version>
|
<version>1.3.5.cnaf.v20191003</version>
|
||||||
<relativePath>..</relativePath>
|
<relativePath>..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>openid-connect-client</artifactId>
|
<artifactId>openid-connect-client</artifactId>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>openid-connect-parent</artifactId>
|
<artifactId>openid-connect-parent</artifactId>
|
||||||
<groupId>org.mitre</groupId>
|
<groupId>org.mitre</groupId>
|
||||||
<version>1.3.5.cnaf.v20190827</version>
|
<version>1.3.5.cnaf.v20191003</version>
|
||||||
<relativePath>..</relativePath>
|
<relativePath>..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>openid-connect-common</artifactId>
|
<artifactId>openid-connect-common</artifactId>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.mitre.openid.connect.service;
|
||||||
|
|
||||||
|
import org.mitre.oauth2.model.ClientDetailsEntity;
|
||||||
|
import org.mitre.openid.connect.exception.ValidationException;
|
||||||
|
|
||||||
|
public interface DynamicClientValidationService {
|
||||||
|
|
||||||
|
public ClientDetailsEntity validateClient(ClientDetailsEntity client) throws ValidationException;
|
||||||
|
|
||||||
|
}
|
|
@ -23,7 +23,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.mitre</groupId>
|
<groupId>org.mitre</groupId>
|
||||||
<artifactId>openid-connect-parent</artifactId>
|
<artifactId>openid-connect-parent</artifactId>
|
||||||
<version>1.3.5.cnaf.v20190827</version>
|
<version>1.3.5.cnaf.v20191003</version>
|
||||||
<relativePath>..</relativePath>
|
<relativePath>..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -0,0 +1,484 @@
|
||||||
|
package org.mitre.openid.connect.service.impl;
|
||||||
|
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.JWKS;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC;
|
||||||
|
import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.mitre.jwt.assertion.AssertionValidator;
|
||||||
|
import org.mitre.oauth2.model.ClientDetailsEntity;
|
||||||
|
import org.mitre.oauth2.model.ClientDetailsEntity.AppType;
|
||||||
|
import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
|
||||||
|
import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType;
|
||||||
|
import org.mitre.oauth2.model.SystemScope;
|
||||||
|
import org.mitre.oauth2.service.ClientDetailsEntityService;
|
||||||
|
import org.mitre.oauth2.service.SystemScopeService;
|
||||||
|
import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
|
||||||
|
import org.mitre.openid.connect.exception.ValidationException;
|
||||||
|
import org.mitre.openid.connect.service.BlacklistedSiteService;
|
||||||
|
import org.mitre.openid.connect.service.DynamicClientValidationService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.oauth2.common.util.OAuth2Utils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.nimbusds.jose.EncryptionMethod;
|
||||||
|
import com.nimbusds.jose.JWEAlgorithm;
|
||||||
|
import com.nimbusds.jose.JWSAlgorithm;
|
||||||
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
|
import com.nimbusds.jwt.JWTClaimsSet;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DefaultDynamicClientValidationService implements DynamicClientValidationService {
|
||||||
|
public static final Logger LOG =
|
||||||
|
LoggerFactory.getLogger(DefaultDynamicClientValidationService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SystemScopeService scopeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("clientAssertionValidator")
|
||||||
|
private AssertionValidator assertionValidator;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlacklistedSiteService blacklistService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConfigurationPropertiesBean config;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClientDetailsEntityService clientService;
|
||||||
|
|
||||||
|
public static final ImmutableSet<String> ALLOWED_GRANT_TYPES =
|
||||||
|
ImmutableSet.of("authorization_code", "implicit", "client_credentials", "refresh_token",
|
||||||
|
"urn:ietf:params:oauth:grant_type:redelegate",
|
||||||
|
"urn:ietf:params:oauth:grant-type:device_code");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientDetailsEntity validateClient(ClientDetailsEntity client) throws ValidationException {
|
||||||
|
|
||||||
|
client = validateSoftwareStatement(client);
|
||||||
|
client = validateScopes(client);
|
||||||
|
client = validateResponseTypes(client);
|
||||||
|
client = validateGrantTypes(client);
|
||||||
|
client = validateRedirectUris(client);
|
||||||
|
client = validateAuth(client);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientDetailsEntity validateScopes(ClientDetailsEntity newClient)
|
||||||
|
throws ValidationException {
|
||||||
|
// scopes that the client is asking for
|
||||||
|
Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());
|
||||||
|
|
||||||
|
// the scopes that the client can have must be a subset of the dynamically allowed scopes
|
||||||
|
Set<SystemScope> allowedScopes =
|
||||||
|
scopeService.removeRestrictedAndReservedScopes(requestedScopes);
|
||||||
|
|
||||||
|
// if the client didn't ask for any, give them the defaults
|
||||||
|
if (allowedScopes == null || allowedScopes.isEmpty()) {
|
||||||
|
allowedScopes = scopeService.getDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
newClient.setScope(scopeService.toStrings(allowedScopes));
|
||||||
|
|
||||||
|
return newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientDetailsEntity validateResponseTypes(ClientDetailsEntity newClient)
|
||||||
|
throws ValidationException {
|
||||||
|
if (newClient.getResponseTypes() == null) {
|
||||||
|
newClient.setResponseTypes(new HashSet<String>());
|
||||||
|
}
|
||||||
|
return newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient)
|
||||||
|
throws ValidationException {
|
||||||
|
// set default grant types if needed
|
||||||
|
if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) {
|
||||||
|
if (newClient.getScope().contains("offline_access")) { // client asked for offline access
|
||||||
|
newClient.setGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow
|
||||||
|
// authorization
|
||||||
|
// code and
|
||||||
|
// refresh
|
||||||
|
// token
|
||||||
|
// grant
|
||||||
|
// types by
|
||||||
|
// default
|
||||||
|
} else {
|
||||||
|
newClient.setGrantTypes(Sets.newHashSet("authorization_code")); // allow authorization code
|
||||||
|
// grant type by default
|
||||||
|
}
|
||||||
|
if (config.isDualClient()) {
|
||||||
|
Set<String> extendedGrandTypes = newClient.getGrantTypes();
|
||||||
|
extendedGrandTypes.add("client_credentials");
|
||||||
|
newClient.setGrantTypes(extendedGrandTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't allow "password" grant type for dynamic registration
|
||||||
|
if (newClient.getGrantTypes().contains("password")) {
|
||||||
|
// return an error, you can't dynamically register for the password grant
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"The password grant type is not allowed in dynamic registration on this server.",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't allow clients to have multiple incompatible grant types and scopes
|
||||||
|
if (newClient.getGrantTypes().contains("authorization_code")) {
|
||||||
|
|
||||||
|
// check for incompatible grants
|
||||||
|
if (newClient.getGrantTypes().contains("implicit")
|
||||||
|
|| (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) {
|
||||||
|
// return an error, you can't have these grant types together
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Incompatible grant types requested: " + newClient.getGrantTypes(),
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newClient.getResponseTypes().contains("token")) {
|
||||||
|
// return an error, you can't have this grant type and response type together
|
||||||
|
throw new ValidationException(
|
||||||
|
"invalid_client_metadata", "Incompatible response types requested: "
|
||||||
|
+ newClient.getGrantTypes() + " / " + newClient.getResponseTypes(),
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
newClient.getResponseTypes().add("code");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newClient.getGrantTypes().contains("implicit")) {
|
||||||
|
|
||||||
|
// check for incompatible grants
|
||||||
|
if (newClient.getGrantTypes().contains("authorization_code")
|
||||||
|
|| (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) {
|
||||||
|
// return an error, you can't have these grant types together
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Incompatible grant types requested: " + newClient.getGrantTypes(),
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newClient.getResponseTypes().contains("code")) {
|
||||||
|
// return an error, you can't have this grant type and response type together
|
||||||
|
throw new ValidationException(
|
||||||
|
"invalid_client_metadata", "Incompatible response types requested: "
|
||||||
|
+ newClient.getGrantTypes() + " / " + newClient.getResponseTypes(),
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
newClient.getResponseTypes().add("token");
|
||||||
|
|
||||||
|
// don't allow refresh tokens in implicit clients
|
||||||
|
newClient.getGrantTypes().remove("refresh_token");
|
||||||
|
newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newClient.getGrantTypes().contains("client_credentials")) {
|
||||||
|
|
||||||
|
// check for incompatible grants
|
||||||
|
if (!config.isDualClient() && (newClient.getGrantTypes().contains("authorization_code")
|
||||||
|
|| newClient.getGrantTypes().contains("implicit"))) {
|
||||||
|
// return an error, you can't have these grant types together
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Incompatible grant types requested: " + newClient.getGrantTypes(),
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newClient.getResponseTypes().isEmpty()) {
|
||||||
|
// return an error, you can't have this grant type and response type together
|
||||||
|
throw new ValidationException(
|
||||||
|
"invalid_client_metadata", "Incompatible response types requested: "
|
||||||
|
+ newClient.getGrantTypes() + " / " + newClient.getResponseTypes(),
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't allow refresh tokens or id tokens in client_credentials clients
|
||||||
|
newClient.getGrantTypes().remove("refresh_token");
|
||||||
|
newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS);
|
||||||
|
newClient.getScope().remove(SystemScopeService.OPENID_SCOPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newClient.getGrantTypes().isEmpty()) {
|
||||||
|
// return an error, you need at least one grant type selected
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Clients must register at least one grant type.", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientDetailsEntity validateRedirectUris(ClientDetailsEntity newClient)
|
||||||
|
throws ValidationException {
|
||||||
|
// check to make sure this client registered a redirect URI if using a redirect flow
|
||||||
|
if (newClient.getGrantTypes().contains("authorization_code")
|
||||||
|
|| newClient.getGrantTypes().contains("implicit")) {
|
||||||
|
if (newClient.getRedirectUris() == null || newClient.getRedirectUris().isEmpty()) {
|
||||||
|
// return an error
|
||||||
|
throw new ValidationException("invalid_redirect_uri",
|
||||||
|
"Clients using a redirect-based grant type must register at least one redirect URI.",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String uri : newClient.getRedirectUris()) {
|
||||||
|
if (blacklistService.isBlacklisted(uri)) {
|
||||||
|
// return an error
|
||||||
|
throw new ValidationException("invalid_redirect_uri",
|
||||||
|
"Redirect URI is not allowed: " + uri, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.contains("#")) {
|
||||||
|
// if it contains the hash symbol then it has a fragment, which isn't allowed
|
||||||
|
throw new ValidationException("invalid_redirect_uri",
|
||||||
|
"Redirect URI can not have a fragment", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientDetailsEntity validateAuth(ClientDetailsEntity newClient)
|
||||||
|
throws ValidationException {
|
||||||
|
if (newClient.getTokenEndpointAuthMethod() == null) {
|
||||||
|
newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC
|
||||||
|
|| newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT
|
||||||
|
|| newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
|
||||||
|
|
||||||
|
if (Strings.isNullOrEmpty(newClient.getClientSecret())) {
|
||||||
|
// no secret yet, we need to generate a secret
|
||||||
|
newClient = clientService.generateClientSecret(newClient);
|
||||||
|
}
|
||||||
|
} else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) {
|
||||||
|
if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) {
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
newClient.setClientSecret(null);
|
||||||
|
} else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) {
|
||||||
|
newClient.setClientSecret(null);
|
||||||
|
} else {
|
||||||
|
throw new ValidationException("invalid_client_metadata", "Unknown authentication method",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param newClient
|
||||||
|
* @return
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
protected ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient)
|
||||||
|
throws ValidationException {
|
||||||
|
if (newClient.getSoftwareStatement() != null) {
|
||||||
|
if (assertionValidator.isValid(newClient.getSoftwareStatement())) {
|
||||||
|
// we have a software statement and its envelope passed all the checks from our validator
|
||||||
|
|
||||||
|
// swap out all of the client's fields for the associated parts of the software statement
|
||||||
|
try {
|
||||||
|
JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet();
|
||||||
|
for (String claim : claimSet.getClaims().keySet()) {
|
||||||
|
switch (claim) {
|
||||||
|
case SOFTWARE_STATEMENT:
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement can't include another software statement",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
case CLAIMS_REDIRECT_URIS:
|
||||||
|
newClient
|
||||||
|
.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case CLIENT_SECRET_EXPIRES_AT:
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement can't include a client secret expiration time",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
case CLIENT_ID_ISSUED_AT:
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement can't include a client ID issuance time",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
case REGISTRATION_CLIENT_URI:
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement can't include a client configuration endpoint",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
case REGISTRATION_ACCESS_TOKEN:
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement can't include a client registration access token",
|
||||||
|
HttpStatus.BAD_REQUEST);
|
||||||
|
case REQUEST_URIS:
|
||||||
|
newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case POST_LOGOUT_REDIRECT_URIS:
|
||||||
|
newClient
|
||||||
|
.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case INITIATE_LOGIN_URI:
|
||||||
|
newClient.setInitiateLoginUri(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case DEFAULT_ACR_VALUES:
|
||||||
|
newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case REQUIRE_AUTH_TIME:
|
||||||
|
newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim));
|
||||||
|
break;
|
||||||
|
case DEFAULT_MAX_AGE:
|
||||||
|
newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim));
|
||||||
|
break;
|
||||||
|
case TOKEN_ENDPOINT_AUTH_SIGNING_ALG:
|
||||||
|
newClient.setTokenEndpointAuthSigningAlg(
|
||||||
|
JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case ID_TOKEN_ENCRYPTED_RESPONSE_ENC:
|
||||||
|
newClient.setIdTokenEncryptedResponseEnc(
|
||||||
|
EncryptionMethod.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case ID_TOKEN_ENCRYPTED_RESPONSE_ALG:
|
||||||
|
newClient.setIdTokenEncryptedResponseAlg(
|
||||||
|
JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case ID_TOKEN_SIGNED_RESPONSE_ALG:
|
||||||
|
newClient
|
||||||
|
.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case USERINFO_ENCRYPTED_RESPONSE_ENC:
|
||||||
|
newClient.setUserInfoEncryptedResponseEnc(
|
||||||
|
EncryptionMethod.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case USERINFO_ENCRYPTED_RESPONSE_ALG:
|
||||||
|
newClient.setUserInfoEncryptedResponseAlg(
|
||||||
|
JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case USERINFO_SIGNED_RESPONSE_ALG:
|
||||||
|
newClient
|
||||||
|
.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case REQUEST_OBJECT_SIGNING_ALG:
|
||||||
|
newClient
|
||||||
|
.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case SUBJECT_TYPE:
|
||||||
|
newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case SECTOR_IDENTIFIER_URI:
|
||||||
|
newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case APPLICATION_TYPE:
|
||||||
|
newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case JWKS_URI:
|
||||||
|
newClient.setJwksUri(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case JWKS:
|
||||||
|
newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString()));
|
||||||
|
break;
|
||||||
|
case POLICY_URI:
|
||||||
|
newClient.setPolicyUri(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case RESPONSE_TYPES:
|
||||||
|
newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case GRANT_TYPES:
|
||||||
|
newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case SCOPE:
|
||||||
|
newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case TOKEN_ENDPOINT_AUTH_METHOD:
|
||||||
|
newClient.setTokenEndpointAuthMethod(
|
||||||
|
AuthMethod.getByValue(claimSet.getStringClaim(claim)));
|
||||||
|
break;
|
||||||
|
case TOS_URI:
|
||||||
|
newClient.setTosUri(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case CONTACTS:
|
||||||
|
newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case LOGO_URI:
|
||||||
|
newClient.setLogoUri(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case CLIENT_URI:
|
||||||
|
newClient.setClientUri(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case CLIENT_NAME:
|
||||||
|
newClient.setClientName(claimSet.getStringClaim(claim));
|
||||||
|
break;
|
||||||
|
case REDIRECT_URIS:
|
||||||
|
newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
||||||
|
break;
|
||||||
|
case CLIENT_SECRET:
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement can't contain client secret", HttpStatus.BAD_REQUEST);
|
||||||
|
case CLIENT_ID:
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement can't contain client ID", HttpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOG.warn("Software statement contained unknown field: " + claim + " with value "
|
||||||
|
+ claimSet.getClaim(claim));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newClient;
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement claims didn't parse", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ValidationException("invalid_client_metadata",
|
||||||
|
"Software statement rejected by validator", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// nothing to see here, carry on
|
||||||
|
return newClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,67 +17,24 @@
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.mitre.openid.connect.web;
|
package org.mitre.openid.connect.web;
|
||||||
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.JWKS;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC;
|
|
||||||
import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.mitre.jwt.assertion.AssertionValidator;
|
import org.mitre.jwt.assertion.AssertionValidator;
|
||||||
import org.mitre.oauth2.model.ClientDetailsEntity;
|
import org.mitre.oauth2.model.ClientDetailsEntity;
|
||||||
import org.mitre.oauth2.model.ClientDetailsEntity.AppType;
|
|
||||||
import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
|
import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
|
||||||
import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType;
|
|
||||||
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
|
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
|
||||||
import org.mitre.oauth2.model.RegisteredClient;
|
import org.mitre.oauth2.model.RegisteredClient;
|
||||||
import org.mitre.oauth2.model.SystemScope;
|
|
||||||
import org.mitre.oauth2.service.ClientDetailsEntityService;
|
import org.mitre.oauth2.service.ClientDetailsEntityService;
|
||||||
import org.mitre.oauth2.service.OAuth2TokenEntityService;
|
import org.mitre.oauth2.service.OAuth2TokenEntityService;
|
||||||
import org.mitre.oauth2.service.SystemScopeService;
|
import org.mitre.oauth2.service.SystemScopeService;
|
||||||
import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor;
|
import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor;
|
||||||
import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
|
import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
|
||||||
import org.mitre.openid.connect.exception.ValidationException;
|
import org.mitre.openid.connect.exception.ValidationException;
|
||||||
import org.mitre.openid.connect.service.BlacklistedSiteService;
|
import org.mitre.openid.connect.service.DynamicClientValidationService;
|
||||||
import org.mitre.openid.connect.service.OIDCTokenService;
|
import org.mitre.openid.connect.service.OIDCTokenService;
|
||||||
import org.mitre.openid.connect.view.ClientInformationResponseView;
|
import org.mitre.openid.connect.view.ClientInformationResponseView;
|
||||||
import org.mitre.openid.connect.view.HttpCodeView;
|
import org.mitre.openid.connect.view.HttpCodeView;
|
||||||
|
@ -89,7 +46,6 @@ import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.oauth2.common.util.OAuth2Utils;
|
|
||||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||||
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
|
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
|
@ -100,15 +56,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.util.UriUtils;
|
import org.springframework.web.util.UriUtils;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.JsonSyntaxException;
|
||||||
import com.nimbusds.jose.EncryptionMethod;
|
|
||||||
import com.nimbusds.jose.JWEAlgorithm;
|
|
||||||
import com.nimbusds.jose.JWSAlgorithm;
|
|
||||||
import com.nimbusds.jose.jwk.JWKSet;
|
|
||||||
import com.nimbusds.jwt.JWTClaimsSet;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping(value = DynamicClientRegistrationEndpoint.URL)
|
@RequestMapping(value = DynamicClientRegistrationEndpoint.URL)
|
||||||
|
@ -122,18 +71,15 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
@Autowired
|
@Autowired
|
||||||
private OAuth2TokenEntityService tokenService;
|
private OAuth2TokenEntityService tokenService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private SystemScopeService scopeService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private BlacklistedSiteService blacklistService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ConfigurationPropertiesBean config;
|
private ConfigurationPropertiesBean config;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private OIDCTokenService connectTokenService;
|
private OIDCTokenService connectTokenService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DynamicClientValidationService clientValidationService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("clientAssertionValidator")
|
@Qualifier("clientAssertionValidator")
|
||||||
private AssertionValidator assertionValidator;
|
private AssertionValidator assertionValidator;
|
||||||
|
@ -141,20 +87,24 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
/**
|
/**
|
||||||
* Logger for this class
|
* Logger for this class
|
||||||
*/
|
*/
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DynamicClientRegistrationEndpoint.class);
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(DynamicClientRegistrationEndpoint.class);
|
||||||
|
|
||||||
public static final ImmutableSet<String> ALLOWED_GRANT_TYPES = ImmutableSet.of(
|
public static final ImmutableSet<String> ALLOWED_GRANT_TYPES =
|
||||||
"authorization_code", "implicit", "client_credentials", "refresh_token",
|
ImmutableSet.of("authorization_code", "implicit", "client_credentials", "refresh_token",
|
||||||
"urn:ietf:params:oauth:grant_type:redelegate",
|
"urn:ietf:params:oauth:grant_type:redelegate",
|
||||||
"urn:ietf:params:oauth:grant-type:device_code");
|
"urn:ietf:params:oauth:grant-type:device_code");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Client, issue a client ID, and create a registration access token.
|
* Create a new Client, issue a client ID, and create a registration access token.
|
||||||
|
*
|
||||||
* @param jsonString
|
* @param jsonString
|
||||||
* @param m
|
* @param m
|
||||||
* @param p
|
* @param p
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||||
|
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public String registerNewClient(@RequestBody String jsonString, Model m) {
|
public String registerNewClient(@RequestBody String jsonString, Model m) {
|
||||||
|
|
||||||
ClientDetailsEntity newClient = null;
|
ClientDetailsEntity newClient = null;
|
||||||
|
@ -185,12 +135,7 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
// do validation on the fields
|
// do validation on the fields
|
||||||
try {
|
try {
|
||||||
newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values
|
newClient = clientValidationService.validateClient(newClient);
|
||||||
newClient = validateScopes(newClient);
|
|
||||||
newClient = validateResponseTypes(newClient);
|
|
||||||
newClient = validateGrantTypes(newClient);
|
|
||||||
newClient = validateRedirectUris(newClient);
|
|
||||||
newClient = validateAuth(newClient);
|
|
||||||
} catch (ValidationException ve) {
|
} catch (ValidationException ve) {
|
||||||
// validation failed, return an error
|
// validation failed, return an error
|
||||||
m.addAttribute(JsonErrorView.ERROR, ve.getError());
|
m.addAttribute(JsonErrorView.ERROR, ve.getError());
|
||||||
|
@ -203,9 +148,9 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
|
newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC ||
|
if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC
|
||||||
newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT ||
|
|| newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT
|
||||||
newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
|
|| newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
|
||||||
|
|
||||||
// we need to generate a secret
|
// we need to generate a secret
|
||||||
newClient = clientService.generateClientSecret(newClient);
|
newClient = clientService.generateClientSecret(newClient);
|
||||||
|
@ -215,21 +160,35 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
if (config.isHeartMode()) {
|
if (config.isHeartMode()) {
|
||||||
// heart mode has different defaults depending on primary grant type
|
// heart mode has different defaults depending on primary grant type
|
||||||
if (newClient.getGrantTypes().contains("authorization_code")) {
|
if (newClient.getGrantTypes().contains("authorization_code")) {
|
||||||
newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr
|
newClient.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1)); // access
|
||||||
newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min
|
// tokens good
|
||||||
newClient.setRefreshTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(24)); // refresh tokens good for 24hr
|
// for 1hr
|
||||||
|
newClient.setIdTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(5)); // id tokens
|
||||||
|
// good for 5min
|
||||||
|
newClient.setRefreshTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(24)); // refresh
|
||||||
|
// tokens
|
||||||
|
// good for
|
||||||
|
// 24hr
|
||||||
} else if (newClient.getGrantTypes().contains("implicit")) {
|
} else if (newClient.getGrantTypes().contains("implicit")) {
|
||||||
newClient.setAccessTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(15)); // access tokens good for 15min
|
newClient.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(15)); // access
|
||||||
newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min
|
// tokens
|
||||||
|
// good for
|
||||||
|
// 15min
|
||||||
|
newClient.setIdTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(5)); // id tokens
|
||||||
|
// good for 5min
|
||||||
newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens
|
newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens
|
||||||
} else if (newClient.getGrantTypes().contains("client_credentials")) {
|
} else if (newClient.getGrantTypes().contains("client_credentials")) {
|
||||||
newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(6)); // access tokens good for 6hr
|
newClient.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(6)); // access
|
||||||
|
// tokens good
|
||||||
|
// for 6hr
|
||||||
newClient.setIdTokenValiditySeconds(0); // no id tokens
|
newClient.setIdTokenValiditySeconds(0); // no id tokens
|
||||||
newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens
|
newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr
|
newClient.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1)); // access tokens
|
||||||
newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min
|
// good for 1hr
|
||||||
|
newClient.setIdTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(10)); // id tokens good
|
||||||
|
// for 10min
|
||||||
newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked
|
newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked
|
||||||
newClient.setDeviceCodeValiditySeconds((int) TimeUnit.MINUTES.toSeconds(10));
|
newClient.setDeviceCodeValiditySeconds((int) TimeUnit.MINUTES.toSeconds(10));
|
||||||
}
|
}
|
||||||
|
@ -246,12 +205,15 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
ClientDetailsEntity savedClient = clientService.saveNewClient(newClient);
|
ClientDetailsEntity savedClient = clientService.saveNewClient(newClient);
|
||||||
|
|
||||||
// generate the registration access token
|
// generate the registration access token
|
||||||
OAuth2AccessTokenEntity token = connectTokenService.createRegistrationAccessToken(savedClient);
|
OAuth2AccessTokenEntity token =
|
||||||
|
connectTokenService.createRegistrationAccessToken(savedClient);
|
||||||
token = tokenService.saveAccessToken(token);
|
token = tokenService.saveAccessToken(token);
|
||||||
|
|
||||||
// send it all out to the view
|
// send it all out to the view
|
||||||
|
|
||||||
RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
|
RegisteredClient registered =
|
||||||
|
new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/"
|
||||||
|
+ UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
|
||||||
m.addAttribute("client", registered);
|
m.addAttribute("client", registered);
|
||||||
m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201
|
m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201
|
||||||
|
|
||||||
|
@ -264,7 +226,8 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
logger.error("Couldn't save client", e);
|
logger.error("Couldn't save client", e);
|
||||||
|
|
||||||
m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
|
m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
|
||||||
m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata.");
|
m.addAttribute(JsonErrorView.ERROR_MESSAGE,
|
||||||
|
"Unable to save client due to invalid or inconsistent metadata.");
|
||||||
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
|
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
|
||||||
|
|
||||||
return JsonErrorView.VIEWNAME;
|
return JsonErrorView.VIEWNAME;
|
||||||
|
@ -281,14 +244,18 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the meta information for a client.
|
* Get the meta information for a client.
|
||||||
|
*
|
||||||
* @param clientId
|
* @param clientId
|
||||||
* @param m
|
* @param m
|
||||||
* @param auth
|
* @param auth
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
|
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('"
|
||||||
@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
+ SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
|
||||||
public String readClientConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
|
@RequestMapping(value = "/{id}", method = RequestMethod.GET,
|
||||||
|
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public String readClientConfiguration(@PathVariable("id") String clientId, Model m,
|
||||||
|
OAuth2Authentication auth) {
|
||||||
|
|
||||||
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
|
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
|
||||||
|
|
||||||
|
@ -296,7 +263,9 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, client);
|
OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, client);
|
||||||
RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8"));
|
RegisteredClient registered =
|
||||||
|
new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/"
|
||||||
|
+ UriUtils.encodePathSegment(client.getClientId(), "UTF-8"));
|
||||||
|
|
||||||
// send it all out to the view
|
// send it all out to the view
|
||||||
m.addAttribute("client", registered);
|
m.addAttribute("client", registered);
|
||||||
|
@ -311,8 +280,8 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// client mismatch
|
// client mismatch
|
||||||
logger.error("readClientConfiguration failed, client ID mismatch: "
|
logger.error("readClientConfiguration failed, client ID mismatch: " + clientId + " and "
|
||||||
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
|
+ auth.getOAuth2Request().getClientId() + " do not match.");
|
||||||
m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
|
m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
|
||||||
|
|
||||||
return HttpCodeView.VIEWNAME;
|
return HttpCodeView.VIEWNAME;
|
||||||
|
@ -321,15 +290,19 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the metainformation for a given client.
|
* Update the metainformation for a given client.
|
||||||
|
*
|
||||||
* @param clientId
|
* @param clientId
|
||||||
* @param jsonString
|
* @param jsonString
|
||||||
* @param m
|
* @param m
|
||||||
* @param auth
|
* @param auth
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
|
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('"
|
||||||
@RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
|
+ SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
|
||||||
public String updateClient(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) {
|
@RequestMapping(value = "/{id}", method = RequestMethod.PUT,
|
||||||
|
produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public String updateClient(@PathVariable("id") String clientId, @RequestBody String jsonString,
|
||||||
|
Model m, OAuth2Authentication auth) {
|
||||||
|
|
||||||
|
|
||||||
ClientDetailsEntity newClient = null;
|
ClientDetailsEntity newClient = null;
|
||||||
|
@ -345,8 +318,12 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId);
|
ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId);
|
||||||
|
|
||||||
if (newClient != null && oldClient != null // we have an existing client and the new one parsed
|
if (newClient != null && oldClient != null // we have an existing client and the new one parsed
|
||||||
&& oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client passed in the URI matches the one in the auth
|
&& oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client
|
||||||
&& oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body matches the one in the URI
|
// passed in the
|
||||||
|
// URI matches the
|
||||||
|
// one in the auth
|
||||||
|
&& oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body
|
||||||
|
// matches the one in the URI
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// a client can't ask to update its own client secret to any particular value
|
// a client can't ask to update its own client secret to any particular value
|
||||||
|
@ -358,7 +335,9 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds());
|
newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds());
|
||||||
newClient.setDeviceCodeValiditySeconds(oldClient.getDeviceCodeValiditySeconds());
|
newClient.setDeviceCodeValiditySeconds(oldClient.getDeviceCodeValiditySeconds());
|
||||||
newClient.setDynamicallyRegistered(true); // it's still dynamically registered
|
newClient.setDynamicallyRegistered(true); // it's still dynamically registered
|
||||||
newClient.setAllowIntrospection(false); // dynamically registered clients can't do introspection -- use the resource registration instead
|
newClient.setAllowIntrospection(false); // dynamically registered clients can't do
|
||||||
|
// introspection -- use the resource registration
|
||||||
|
// instead
|
||||||
newClient.setAuthorities(oldClient.getAuthorities());
|
newClient.setAuthorities(oldClient.getAuthorities());
|
||||||
newClient.setClientDescription(oldClient.getClientDescription());
|
newClient.setClientDescription(oldClient.getClientDescription());
|
||||||
newClient.setCreatedAt(oldClient.getCreatedAt());
|
newClient.setCreatedAt(oldClient.getCreatedAt());
|
||||||
|
@ -373,12 +352,7 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
// do validation on the fields
|
// do validation on the fields
|
||||||
try {
|
try {
|
||||||
newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values
|
newClient = clientValidationService.validateClient(newClient);
|
||||||
newClient = validateScopes(newClient);
|
|
||||||
newClient = validateResponseTypes(newClient);
|
|
||||||
newClient = validateGrantTypes(newClient);
|
|
||||||
newClient = validateRedirectUris(newClient);
|
|
||||||
newClient = validateAuth(newClient);
|
|
||||||
} catch (ValidationException ve) {
|
} catch (ValidationException ve) {
|
||||||
// validation failed, return an error
|
// validation failed, return an error
|
||||||
m.addAttribute(JsonErrorView.ERROR, ve.getError());
|
m.addAttribute(JsonErrorView.ERROR, ve.getError());
|
||||||
|
@ -397,7 +371,9 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, savedClient);
|
OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, savedClient);
|
||||||
|
|
||||||
RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
|
RegisteredClient registered =
|
||||||
|
new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/"
|
||||||
|
+ UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
|
||||||
|
|
||||||
// send it all out to the view
|
// send it all out to the view
|
||||||
m.addAttribute("client", registered);
|
m.addAttribute("client", registered);
|
||||||
|
@ -412,15 +388,16 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
logger.error("Couldn't save client", e);
|
logger.error("Couldn't save client", e);
|
||||||
|
|
||||||
m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
|
m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
|
||||||
m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata.");
|
m.addAttribute(JsonErrorView.ERROR_MESSAGE,
|
||||||
|
"Unable to save client due to invalid or inconsistent metadata.");
|
||||||
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
|
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
|
||||||
|
|
||||||
return JsonErrorView.VIEWNAME;
|
return JsonErrorView.VIEWNAME;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// client mismatch
|
// client mismatch
|
||||||
logger.error("updateClient failed, client ID mismatch: "
|
logger.error("updateClient failed, client ID mismatch: " + clientId + " and "
|
||||||
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
|
+ auth.getOAuth2Request().getClientId() + " do not match.");
|
||||||
m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
|
m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
|
||||||
|
|
||||||
return HttpCodeView.VIEWNAME;
|
return HttpCodeView.VIEWNAME;
|
||||||
|
@ -429,14 +406,18 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the indicated client from the system.
|
* Delete the indicated client from the system.
|
||||||
|
*
|
||||||
* @param clientId
|
* @param clientId
|
||||||
* @param m
|
* @param m
|
||||||
* @param auth
|
* @param auth
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
|
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('"
|
||||||
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
|
+ SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
|
||||||
public String deleteClient(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
|
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE,
|
||||||
|
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public String deleteClient(@PathVariable("id") String clientId, Model m,
|
||||||
|
OAuth2Authentication auth) {
|
||||||
|
|
||||||
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
|
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
|
||||||
|
|
||||||
|
@ -449,326 +430,19 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
return HttpCodeView.VIEWNAME;
|
return HttpCodeView.VIEWNAME;
|
||||||
} else {
|
} else {
|
||||||
// client mismatch
|
// client mismatch
|
||||||
logger.error("readClientConfiguration failed, client ID mismatch: "
|
logger.error("readClientConfiguration failed, client ID mismatch: " + clientId + " and "
|
||||||
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
|
+ auth.getOAuth2Request().getClientId() + " do not match.");
|
||||||
m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
|
m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
|
||||||
|
|
||||||
return HttpCodeView.VIEWNAME;
|
return HttpCodeView.VIEWNAME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException {
|
|
||||||
// scopes that the client is asking for
|
|
||||||
Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());
|
|
||||||
|
|
||||||
// the scopes that the client can have must be a subset of the dynamically allowed scopes
|
|
||||||
Set<SystemScope> allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes);
|
|
||||||
|
|
||||||
// if the client didn't ask for any, give them the defaults
|
|
||||||
if (allowedScopes == null || allowedScopes.isEmpty()) {
|
|
||||||
allowedScopes = scopeService.getDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
newClient.setScope(scopeService.toStrings(allowedScopes));
|
|
||||||
|
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientDetailsEntity validateResponseTypes(ClientDetailsEntity newClient) throws ValidationException {
|
|
||||||
if (newClient.getResponseTypes() == null) {
|
|
||||||
newClient.setResponseTypes(new HashSet<String>());
|
|
||||||
}
|
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient)
|
|
||||||
throws ValidationException {
|
|
||||||
// set default grant types if needed
|
|
||||||
if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) {
|
|
||||||
if (newClient.getScope().contains("offline_access")) { // client asked for offline access
|
|
||||||
newClient.setGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authorization code and refresh token grant types by default
|
|
||||||
} else {
|
|
||||||
newClient.setGrantTypes(Sets.newHashSet("authorization_code")); // allow authorization code grant type by default
|
|
||||||
}
|
|
||||||
if (config.isDualClient()) {
|
|
||||||
Set<String> extendedGrandTypes = newClient.getGrantTypes();
|
|
||||||
extendedGrandTypes.add("client_credentials");
|
|
||||||
newClient.setGrantTypes(extendedGrandTypes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't allow "password" grant type for dynamic registration
|
|
||||||
if (newClient.getGrantTypes().contains("password")) {
|
|
||||||
// return an error, you can't dynamically register for the password grant
|
|
||||||
throw new ValidationException("invalid_client_metadata", "The password grant type is not allowed in dynamic registration on this server.", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't allow clients to have multiple incompatible grant types and scopes
|
|
||||||
if (newClient.getGrantTypes().contains("authorization_code")) {
|
|
||||||
|
|
||||||
// check for incompatible grants
|
|
||||||
if (newClient.getGrantTypes().contains("implicit") ||
|
|
||||||
(!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) {
|
|
||||||
// return an error, you can't have these grant types together
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newClient.getResponseTypes().contains("token")) {
|
|
||||||
// return an error, you can't have this grant type and response type together
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
newClient.getResponseTypes().add("code");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newClient.getGrantTypes().contains("implicit")) {
|
|
||||||
|
|
||||||
// check for incompatible grants
|
|
||||||
if (newClient.getGrantTypes().contains("authorization_code") ||
|
|
||||||
(!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) {
|
|
||||||
// return an error, you can't have these grant types together
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newClient.getResponseTypes().contains("code")) {
|
|
||||||
// return an error, you can't have this grant type and response type together
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
newClient.getResponseTypes().add("token");
|
|
||||||
|
|
||||||
// don't allow refresh tokens in implicit clients
|
|
||||||
newClient.getGrantTypes().remove("refresh_token");
|
|
||||||
newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newClient.getGrantTypes().contains("client_credentials")) {
|
|
||||||
|
|
||||||
// check for incompatible grants
|
|
||||||
if (!config.isDualClient() &&
|
|
||||||
(newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit"))) {
|
|
||||||
// return an error, you can't have these grant types together
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newClient.getResponseTypes().isEmpty()) {
|
|
||||||
// return an error, you can't have this grant type and response type together
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't allow refresh tokens or id tokens in client_credentials clients
|
|
||||||
newClient.getGrantTypes().remove("refresh_token");
|
|
||||||
newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS);
|
|
||||||
newClient.getScope().remove(SystemScopeService.OPENID_SCOPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newClient.getGrantTypes().isEmpty()) {
|
|
||||||
// return an error, you need at least one grant type selected
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Clients must register at least one grant type.", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientDetailsEntity validateRedirectUris(ClientDetailsEntity newClient) throws ValidationException {
|
|
||||||
// check to make sure this client registered a redirect URI if using a redirect flow
|
|
||||||
if (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit")) {
|
|
||||||
if (newClient.getRedirectUris() == null || newClient.getRedirectUris().isEmpty()) {
|
|
||||||
// return an error
|
|
||||||
throw new ValidationException("invalid_redirect_uri", "Clients using a redirect-based grant type must register at least one redirect URI.", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String uri : newClient.getRedirectUris()) {
|
|
||||||
if (blacklistService.isBlacklisted(uri)) {
|
|
||||||
// return an error
|
|
||||||
throw new ValidationException("invalid_redirect_uri", "Redirect URI is not allowed: " + uri, HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.contains("#")) {
|
|
||||||
// if it contains the hash symbol then it has a fragment, which isn't allowed
|
|
||||||
throw new ValidationException("invalid_redirect_uri", "Redirect URI can not have a fragment", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws ValidationException {
|
|
||||||
if (newClient.getTokenEndpointAuthMethod() == null) {
|
|
||||||
newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC ||
|
|
||||||
newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT ||
|
|
||||||
newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
|
|
||||||
|
|
||||||
if (Strings.isNullOrEmpty(newClient.getClientSecret())) {
|
|
||||||
// no secret yet, we need to generate a secret
|
|
||||||
newClient = clientService.generateClientSecret(newClient);
|
|
||||||
}
|
|
||||||
} else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) {
|
|
||||||
if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) {
|
|
||||||
throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
newClient.setClientSecret(null);
|
|
||||||
} else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) {
|
|
||||||
newClient.setClientSecret(null);
|
|
||||||
} else {
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Unknown authentication method", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param newClient
|
|
||||||
* @return
|
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
|
||||||
private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException {
|
|
||||||
if (newClient.getSoftwareStatement() != null) {
|
|
||||||
if (assertionValidator.isValid(newClient.getSoftwareStatement())) {
|
|
||||||
// we have a software statement and its envelope passed all the checks from our validator
|
|
||||||
|
|
||||||
// swap out all of the client's fields for the associated parts of the software statement
|
|
||||||
try {
|
|
||||||
JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet();
|
|
||||||
for (String claim : claimSet.getClaims().keySet()) {
|
|
||||||
switch (claim) {
|
|
||||||
case SOFTWARE_STATEMENT:
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST);
|
|
||||||
case CLAIMS_REDIRECT_URIS:
|
|
||||||
newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case CLIENT_SECRET_EXPIRES_AT:
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST);
|
|
||||||
case CLIENT_ID_ISSUED_AT:
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST);
|
|
||||||
case REGISTRATION_CLIENT_URI:
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST);
|
|
||||||
case REGISTRATION_ACCESS_TOKEN:
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST);
|
|
||||||
case REQUEST_URIS:
|
|
||||||
newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case POST_LOGOUT_REDIRECT_URIS:
|
|
||||||
newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case INITIATE_LOGIN_URI:
|
|
||||||
newClient.setInitiateLoginUri(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case DEFAULT_ACR_VALUES:
|
|
||||||
newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case REQUIRE_AUTH_TIME:
|
|
||||||
newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim));
|
|
||||||
break;
|
|
||||||
case DEFAULT_MAX_AGE:
|
|
||||||
newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim));
|
|
||||||
break;
|
|
||||||
case TOKEN_ENDPOINT_AUTH_SIGNING_ALG:
|
|
||||||
newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case ID_TOKEN_ENCRYPTED_RESPONSE_ENC:
|
|
||||||
newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case ID_TOKEN_ENCRYPTED_RESPONSE_ALG:
|
|
||||||
newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case ID_TOKEN_SIGNED_RESPONSE_ALG:
|
|
||||||
newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case USERINFO_ENCRYPTED_RESPONSE_ENC:
|
|
||||||
newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case USERINFO_ENCRYPTED_RESPONSE_ALG:
|
|
||||||
newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case USERINFO_SIGNED_RESPONSE_ALG:
|
|
||||||
newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case REQUEST_OBJECT_SIGNING_ALG:
|
|
||||||
newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case SUBJECT_TYPE:
|
|
||||||
newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case SECTOR_IDENTIFIER_URI:
|
|
||||||
newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case APPLICATION_TYPE:
|
|
||||||
newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case JWKS_URI:
|
|
||||||
newClient.setJwksUri(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case JWKS:
|
|
||||||
newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString()));
|
|
||||||
break;
|
|
||||||
case POLICY_URI:
|
|
||||||
newClient.setPolicyUri(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case RESPONSE_TYPES:
|
|
||||||
newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case GRANT_TYPES:
|
|
||||||
newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case SCOPE:
|
|
||||||
newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case TOKEN_ENDPOINT_AUTH_METHOD:
|
|
||||||
newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim)));
|
|
||||||
break;
|
|
||||||
case TOS_URI:
|
|
||||||
newClient.setTosUri(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case CONTACTS:
|
|
||||||
newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case LOGO_URI:
|
|
||||||
newClient.setLogoUri(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case CLIENT_URI:
|
|
||||||
newClient.setClientUri(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case CLIENT_NAME:
|
|
||||||
newClient.setClientName(claimSet.getStringClaim(claim));
|
|
||||||
break;
|
|
||||||
case REDIRECT_URIS:
|
|
||||||
newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
|
|
||||||
break;
|
|
||||||
case CLIENT_SECRET:
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST);
|
|
||||||
case CLIENT_ID:
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST);
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newClient;
|
|
||||||
} catch (ParseException e) {
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// nothing to see here, carry on
|
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Rotates the registration token if it's expired, otherwise returns it
|
* Rotates the registration token if it's expired, otherwise returns it
|
||||||
*/
|
*/
|
||||||
private OAuth2AccessTokenEntity rotateRegistrationTokenIfNecessary(OAuth2Authentication auth, ClientDetailsEntity client) {
|
private OAuth2AccessTokenEntity rotateRegistrationTokenIfNecessary(OAuth2Authentication auth,
|
||||||
|
ClientDetailsEntity client) {
|
||||||
|
|
||||||
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
|
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
|
||||||
OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue());
|
OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue());
|
||||||
|
@ -777,11 +451,13 @@ public class DynamicClientRegistrationEndpoint {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Re-issue the token if it has been issued before [currentTime - validity]
|
// Re-issue the token if it has been issued before [currentTime - validity]
|
||||||
Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000);
|
Date validToDate =
|
||||||
|
new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000);
|
||||||
if (token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) {
|
if (token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) {
|
||||||
logger.info("Rotating the registration access token for " + client.getClientId());
|
logger.info("Rotating the registration access token for " + client.getClientId());
|
||||||
tokenService.revokeAccessToken(token);
|
tokenService.revokeAccessToken(token);
|
||||||
OAuth2AccessTokenEntity newToken = connectTokenService.createRegistrationAccessToken(client);
|
OAuth2AccessTokenEntity newToken =
|
||||||
|
connectTokenService.createRegistrationAccessToken(client);
|
||||||
tokenService.saveAccessToken(newToken);
|
tokenService.saveAccessToken(newToken);
|
||||||
return newToken;
|
return newToken;
|
||||||
} else {
|
} else {
|
||||||
|
|
2
pom.xml
2
pom.xml
|
@ -20,7 +20,7 @@
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.mitre</groupId>
|
<groupId>org.mitre</groupId>
|
||||||
<artifactId>openid-connect-parent</artifactId>
|
<artifactId>openid-connect-parent</artifactId>
|
||||||
<version>1.3.5.cnaf.v20190827</version>
|
<version>1.3.5.cnaf.v20191003</version>
|
||||||
<name>MITREid Connect</name>
|
<name>MITREid Connect</name>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<parent>
|
<parent>
|
||||||
|
|
Loading…
Reference in New Issue