/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.cxf.rs.security.oauth2.services; import java.util.Collections; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; import org.apache.cxf.common.util.Base64UrlUtility; import org.apache.cxf.common.util.StringUtils; import org.apache.cxf.jaxrs.ext.MessageContext; import org.apache.cxf.jaxrs.utils.ExceptionUtils; import org.apache.cxf.rs.security.oauth2.common.Client; import org.apache.cxf.rs.security.oauth2.common.UserSubject; import org.apache.cxf.rs.security.oauth2.provider.ClientRegistrationProvider; import org.apache.cxf.rs.security.oauth2.utils.AuthorizationUtils; import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants; import org.apache.cxf.rs.security.oauth2.utils.OAuthUtils; import org.apache.cxf.rt.security.crypto.CryptoUtils; @Path("register") public class DynamicRegistrationService { private static final String DEFAULT_APPLICATION_TYPE = "web"; private static final Integer DEFAULT_CLIENT_ID_SIZE = 10; private ClientRegistrationProvider clientProvider; private String initialAccessToken; private int clientIdSizeInBytes = DEFAULT_CLIENT_ID_SIZE; private MessageContext mc; private boolean supportRegistrationAccessTokens = true; private String userRole; @POST @Consumes("application/json") @Produces("application/json") public Response register(ClientRegistration request) { checkInitialAuthentication(); Client client = createNewClient(request); createRegAccessToken(client); clientProvider.setClient(client); return Response.status(201).entity(fromClientToRegistrationResponse(client)).build(); } protected void checkInitialAuthentication() { if (initialAccessToken != null) { String accessToken = getRequestAccessToken(); if (!initialAccessToken.equals(accessToken)) { throw ExceptionUtils.toNotAuthorizedException(null, null); } } else { checkSecurityContext(); } } protected void checkSecurityContext() { SecurityContext sc = mc.getSecurityContext(); if (sc.getUserPrincipal() == null) { throw ExceptionUtils.toNotAuthorizedException(null, null); } if (userRole != null && !sc.isUserInRole(userRole)) { throw ExceptionUtils.toForbiddenException(null, null); } } protected String createRegAccessToken(Client client) { String regAccessToken = OAuthUtils.generateRandomTokenKey(); client.getProperties().put(ClientRegistrationResponse.REG_ACCESS_TOKEN, regAccessToken); return regAccessToken; } protected void checkRegistrationAccessToken(Client c, String accessToken) { String regAccessToken = c.getProperties().get(ClientRegistrationResponse.REG_ACCESS_TOKEN); if (regAccessToken == null || !regAccessToken.equals(accessToken)) { throw ExceptionUtils.toNotAuthorizedException(null, null); } } @GET @Produces("application/json") public ClientRegistration readClientRegistrationWithQuery(@QueryParam("client_id") String clientId) { return doReadClientRegistration(clientId); } @GET @Path("{clientId}") @Produces("application/json") public ClientRegistration readClientRegistrationWithPath(@PathParam("clientId") String clientId) { return doReadClientRegistration(clientId); } @PUT @Path("{clientId}") @Consumes("application/json") public Response updateClientRegistration(@PathParam("clientId") String clientId) { return Response.ok().build(); } @DELETE @Path("{clientId}") public Response deleteClientRegistration(@PathParam("clientId") String clientId) { if (readClient(clientId) != null) { clientProvider.removeClient(clientId); } return Response.ok().build(); } protected ClientRegistrationResponse fromClientToRegistrationResponse(Client client) { ClientRegistrationResponse response = new ClientRegistrationResponse(); response.setClientId(client.getClientId()); if (client.getClientSecret() != null) { response.setClientSecret(client.getClientSecret()); // TODO: consider making Client secret time limited response.setClientSecretExpiresAt(Long.valueOf(0)); } response.setClientIdIssuedAt(client.getRegisteredAt()); UriBuilder ub = getMessageContext().getUriInfo().getAbsolutePathBuilder(); if (supportRegistrationAccessTokens) { // both registration access token and uri are either included or excluded response.setRegistrationClientUri( ub.path(client.getClientId()).build().toString()); response.setRegistrationAccessToken( client.getProperties().get(ClientRegistrationResponse.REG_ACCESS_TOKEN)); } return response; } protected ClientRegistration doReadClientRegistration(String clientId) { Client client = readClient(clientId); return fromClientToClientRegistration(client); } protected ClientRegistration fromClientToClientRegistration(Client c) { ClientRegistration reg = new ClientRegistration(); reg.setClientName(c.getApplicationName()); reg.setGrantTypes(c.getAllowedGrantTypes()); reg.setApplicationType(c.isConfidential() ? "web" : "native"); reg.setRedirectUris(c.getRedirectUris()); reg.setScope(OAuthUtils.convertListOfScopesToString(c.getRegisteredScopes())); if (c.getApplicationWebUri() != null) { reg.setClientUri(c.getApplicationWebUri()); } if (c.getApplicationLogoUri() != null) { reg.setLogoUri(c.getApplicationLogoUri()); } if (!c.getRegisteredAudiences().isEmpty()) { reg.setResourceUris(c.getRegisteredAudiences()); } if (c.getTokenEndpointAuthMethod() != null) { reg.setTokenEndpointAuthMethod(c.getTokenEndpointAuthMethod()); if (OAuthConstants.TOKEN_ENDPOINT_AUTH_TLS.equals(c.getTokenEndpointAuthMethod())) { String subjectDn = c.getProperties().get(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN); if (subjectDn != null) { reg.setProperty(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN, subjectDn); } String issuerDn = c.getProperties().get(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN); if (issuerDn != null) { reg.setProperty(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN, issuerDn); } } } return reg; } protected Client readClient(String clientId) { String accessToken = getRequestAccessToken(); Client c = clientProvider.getClient(clientId); if (c == null) { throw ExceptionUtils.toNotAuthorizedException(null, null); } checkRegistrationAccessToken(c, accessToken); return c; } public String getInitialAccessToken() { return initialAccessToken; } public void setInitialAccessToken(String initialAccessToken) { this.initialAccessToken = initialAccessToken; } protected Client createNewClient(ClientRegistration request) { // Client ID String clientId = generateClientId(); // Client Name String clientName = request.getClientName(); if (StringUtils.isEmpty(clientName)) { clientName = clientId; } List<String> grantTypes = request.getGrantTypes(); if (grantTypes == null) { grantTypes = Collections.singletonList("authorization_code"); } String tokenEndpointAuthMethod = request.getTokenEndpointAuthMethod(); //TODO: default is expected to be set to OAuthConstants.TOKEN_ENDPOINT_AUTH_BASIC boolean passwordRequired = !grantTypes.contains(OAuthConstants.IMPLICIT_GRANT) && (tokenEndpointAuthMethod == null || OAuthConstants.TOKEN_ENDPOINT_AUTH_BASIC.equals(tokenEndpointAuthMethod) || OAuthConstants.TOKEN_ENDPOINT_AUTH_POST.equals(tokenEndpointAuthMethod)); // Application Type // https://tools.ietf.org/html/rfc7591 has no this property but // but http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata does String appType = request.getApplicationType(); if (appType == null) { appType = DEFAULT_APPLICATION_TYPE; } boolean isConfidential = DEFAULT_APPLICATION_TYPE.equals(appType) && (passwordRequired || OAuthConstants.TOKEN_ENDPOINT_AUTH_TLS.equals(tokenEndpointAuthMethod)); // Client Secret String clientSecret = passwordRequired ? generateClientSecret(request) : null; Client newClient = new Client(clientId, clientSecret, isConfidential, clientName); newClient.setAllowedGrantTypes(grantTypes); newClient.setTokenEndpointAuthMethod(tokenEndpointAuthMethod); if (OAuthConstants.TOKEN_ENDPOINT_AUTH_TLS.equals(tokenEndpointAuthMethod)) { String subjectDn = (String)request.getProperty(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN); if (subjectDn != null) { newClient.getProperties().put(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN, subjectDn); } String issuerDn = (String)request.getProperty(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN); if (issuerDn != null) { newClient.getProperties().put(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN, issuerDn); } } // Client Registration Time newClient.setRegisteredAt(System.currentTimeMillis() / 1000); // Client Redirect URIs List<String> redirectUris = request.getRedirectUris(); if (redirectUris != null) { for (String uri : redirectUris) { validateRequestUri(uri, appType, grantTypes); } newClient.setRedirectUris(redirectUris); } // Client Resource Audience URIs List<String> resourceUris = request.getResourceUris(); if (resourceUris != null) { newClient.setRegisteredAudiences(resourceUris); } // Client Scopes String scope = request.getScope(); if (!StringUtils.isEmpty(scope)) { newClient.setRegisteredScopes(OAuthUtils.parseScope(scope)); } // Client Application URI String clientUri = request.getClientUri(); if (clientUri != null) { newClient.setApplicationWebUri(clientUri); } // Client Logo URI String clientLogoUri = request.getLogoUri(); if (clientLogoUri != null) { newClient.setApplicationLogoUri(clientLogoUri); } //TODO: check other properties // Add more typed properties like tosUri, policyUri, etc to Client // or set them as Client extra properties SecurityContext sc = mc.getSecurityContext(); if (sc != null && sc.getUserPrincipal() != null && sc.getUserPrincipal().getName() != null) { UserSubject subject = new UserSubject(sc.getUserPrincipal().getName()); newClient.setResourceOwnerSubject(subject); } newClient.setRegisteredDynamically(true); return newClient; } protected void validateRequestUri(String uri, String appType, List<String> grantTypes) { // Web Clients using the OAuth Implicit Grant Type MUST only register URLs using the https scheme // as redirect_uris; they MUST NOT use localhost as the hostname. Native Clients MUST only register // redirect_uris using custom URI schemes or URLs using the http: scheme with localhost as the hostname. // Authorization Servers MAY place additional constraints on Native Clients. Authorization Servers MAY // reject Redirection URI values using the http scheme, other than the localhost case for Native Clients } public void setClientProvider(ClientRegistrationProvider clientProvider) { this.clientProvider = clientProvider; } protected String generateClientId() { return Base64UrlUtility.encode( CryptoUtils.generateSecureRandomBytes( getClientIdSizeInBytes())); } public int getClientIdSizeInBytes() { return clientIdSizeInBytes; } public void setClientIdSizeInBytes(int size) { clientIdSizeInBytes = size; } protected String generateClientSecret(ClientRegistration request) { return Base64UrlUtility.encode( CryptoUtils.generateSecureRandomBytes( getClientSecretSizeInBytes(request))); } protected String getRequestAccessToken() { // This call will throw 401 if no given authorization scheme exists return AuthorizationUtils.getAuthorizationParts(getMessageContext(), Collections.singleton(OAuthConstants.BEARER_AUTHORIZATION_SCHEME))[1]; } protected int getClientSecretSizeInBytes(ClientRegistration request) { return 16; } @Context public void setMessageContext(MessageContext context) { this.mc = context; } public MessageContext getMessageContext() { return mc; } public void setSupportRegistrationAccessTokens(boolean supportRegistrationAccessTokens) { this.supportRegistrationAccessTokens = supportRegistrationAccessTokens; } public void setUserRole(String userRole) { this.userRole = userRole; } }