/** * 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.client; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.ResponseProcessingException; import javax.ws.rs.core.Form; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import org.apache.cxf.common.util.Base64Utility; import org.apache.cxf.common.util.StringUtils; import org.apache.cxf.jaxrs.client.WebClient; import org.apache.cxf.rs.security.oauth2.common.AccessTokenGrant; import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken; import org.apache.cxf.rs.security.oauth2.common.OAuthError; import org.apache.cxf.rs.security.oauth2.grants.refresh.RefreshTokenGrant; import org.apache.cxf.rs.security.oauth2.provider.OAuthJSONProvider; import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException; import org.apache.cxf.rs.security.oauth2.tokens.hawk.HawkAuthorizationScheme; import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants; /** * The utility class for simplifying working with OAuth servers */ public final class OAuthClientUtils { private OAuthClientUtils() { } /** * Builds a complete URI for redirecting to OAuth Authorization Service * @param authorizationServiceURI the service endpoint address * @param clientId client registration id * @param redirectUri the uri the authorization code will be posted to * @param state the client state, example the key or the encrypted token * representing the info about the current end user's request * @scope scope the optional scope; if not specified then the authorization * service will allocate the default scope * @return authorization service URI */ public static URI getAuthorizationURI(String authorizationServiceURI, String clientId, String redirectUri, String state, String scope) { UriBuilder ub = getAuthorizationURIBuilder(authorizationServiceURI, clientId, redirectUri, state, scope); if (redirectUri != null) { ub.queryParam(OAuthConstants.REDIRECT_URI, redirectUri); } if (state != null) { ub.queryParam(OAuthConstants.STATE, state); } return ub.build(); } public static UriBuilder getAuthorizationURIBuilder(String authorizationServiceURI, String clientId, String redirectUri, String state, String scope) { UriBuilder ub = getAuthorizationURIBuilder(authorizationServiceURI, clientId, scope); if (redirectUri != null) { ub.queryParam(OAuthConstants.REDIRECT_URI, redirectUri); } if (state != null) { ub.queryParam(OAuthConstants.STATE, state); } return ub; } /** * Creates the builder for building OAuth AuthorizationService URIs * @param authorizationServiceURI the service endpoint address * @param clientId client registration id * @param scope the optional scope; if not specified then the authorization * service will allocate the default scope * @return the builder */ public static UriBuilder getAuthorizationURIBuilder(String authorizationServiceURI, String clientId, String scope) { UriBuilder ub = UriBuilder.fromUri(authorizationServiceURI); if (clientId != null) { ub.queryParam(OAuthConstants.CLIENT_ID, clientId); } if (scope != null) { ub.queryParam(OAuthConstants.SCOPE, scope); } ub.queryParam(OAuthConstants.RESPONSE_TYPE, OAuthConstants.CODE_RESPONSE_TYPE); return ub; } /** * Obtains the access token from OAuth AccessToken Service * using the initialized web client * @param accessTokenService the AccessToken client * @param consumer {@link Consumer} representing the registered client * @param grant {@link AccessTokenGrant} grant * @return {@link ClientAccessToken} access token * @throws OAuthServiceException */ public static ClientAccessToken getAccessToken(WebClient accessTokenService, Consumer consumer, AccessTokenGrant grant) throws OAuthServiceException { return getAccessToken(accessTokenService, consumer, grant, true); } /** * Obtains the access token from OAuth AccessToken Service * @param accessTokenServiceUri the AccessToken endpoint address * @param consumer {@link Consumer} representing the registered client * @param grant {@link AccessTokenGrant} grant * @param setAuthorizationHeader if set to true then HTTP Basic scheme * will be used to pass client id and secret, otherwise they will * be passed in the form payload * @return {@link ClientAccessToken} access token * @throws OAuthServiceException */ public static ClientAccessToken getAccessToken(String accessTokenServiceUri, Consumer consumer, AccessTokenGrant grant, boolean setAuthorizationHeader) throws OAuthServiceException { OAuthJSONProvider provider = new OAuthJSONProvider(); WebClient accessTokenService = WebClient.create(accessTokenServiceUri, Collections.singletonList(provider)); accessTokenService.accept("application/json"); return getAccessToken(accessTokenService, consumer, grant, setAuthorizationHeader); } /** * Obtains the access token from OAuth AccessToken Service * using the initialized web client * @param accessTokenService the AccessToken client * @param consumer {@link Consumer} representing the registered client. * @param grant {@link AccessTokenGrant} grant * @param setAuthorizationHeader if set to true then HTTP Basic scheme * will be used to pass client id and secret, otherwise they will * be passed in the form payload * @return {@link ClientAccessToken} access token * @throws OAuthServiceException */ public static ClientAccessToken getAccessToken(WebClient accessTokenService, Consumer consumer, AccessTokenGrant grant, boolean setAuthorizationHeader) { return getAccessToken(accessTokenService, consumer, grant, null, setAuthorizationHeader); } /** * Obtains the access token from OAuth AccessToken Service * using the initialized web client * @param accessTokenService the AccessToken client * @param grant {@link AccessTokenGrant} grant * @return {@link ClientAccessToken} access token * @throws OAuthServiceException */ public static ClientAccessToken getAccessToken(WebClient accessTokenService, AccessTokenGrant grant) throws OAuthServiceException { return getAccessToken(accessTokenService, null, grant, null, false); } /** * Obtains the access token from OAuth AccessToken Service * using the initialized web client * @param accessTokenService the AccessToken client * @param grant {@link AccessTokenGrant} grant * @param extraParams extra parameters * @return {@link ClientAccessToken} access token * @throws OAuthServiceException */ public static ClientAccessToken getAccessToken(WebClient accessTokenService, AccessTokenGrant grant, Map<String, String> extraParams) throws OAuthServiceException { return getAccessToken(accessTokenService, null, grant, extraParams, false); } /** * Obtains the access token from OAuth AccessToken Service * using the initialized web client * @param accessTokenService the AccessToken client * @param consumer {@link Consumer} representing the registered client. * @param grant {@link AccessTokenGrant} grant * @param extraParams extra parameters * @param setAuthorizationHeader if set to true then HTTP Basic scheme * will be used to pass client id and secret, otherwise they will * be passed in the form payload * @return {@link ClientAccessToken} access token * @throws OAuthServiceException */ public static ClientAccessToken getAccessToken(WebClient accessTokenService, Consumer consumer, AccessTokenGrant grant, Map<String, String> extraParams, boolean setAuthorizationHeader) throws OAuthServiceException { return getAccessToken(accessTokenService, consumer, grant, extraParams, null, setAuthorizationHeader); } public static ClientAccessToken refreshAccessToken(WebClient accessTokenService, ClientAccessToken at) { return refreshAccessToken(accessTokenService, null, at, null, true); } public static ClientAccessToken refreshAccessToken(WebClient accessTokenService, Consumer consumer, ClientAccessToken at) { return refreshAccessToken(accessTokenService, consumer, at, null, true); } public static ClientAccessToken refreshAccessToken(WebClient accessTokenService, Consumer consumer, ClientAccessToken at, String scope, boolean setAuthorizationHeader) throws OAuthServiceException { RefreshTokenGrant grant = new RefreshTokenGrant(at.getRefreshToken(), scope); return getAccessToken(accessTokenService, consumer, grant, null, at.getTokenType(), setAuthorizationHeader); } /** * Obtains the access token from OAuth AccessToken Service * using the initialized web client * @param accessTokenService the AccessToken client * @param consumer {@link Consumer} representing the registered client. * @param grant {@link AccessTokenGrant} grant * @param extraParams extra parameters * @param defaultTokenType default expected token type - some early * well-known OAuth2 services do not return a required token_type parameter * @param setAuthorizationHeader if set to true then HTTP Basic scheme * will be used to pass client id and secret, otherwise they will * be passed in the form payload * @return {@link ClientAccessToken} access token * @throws OAuthServiceException */ public static ClientAccessToken getAccessToken(WebClient accessTokenService, Consumer consumer, AccessTokenGrant grant, Map<String, String> extraParams, String defaultTokenType, boolean setAuthorizationHeader) throws OAuthServiceException { if (accessTokenService == null) { throw new OAuthServiceException(OAuthConstants.SERVER_ERROR); } Form form = new Form(grant.toMap()); if (extraParams != null) { for (Map.Entry<String, String> entry : extraParams.entrySet()) { form.param(entry.getKey(), entry.getValue()); } } if (consumer != null) { boolean secretAvailable = !StringUtils.isEmpty(consumer.getClientSecret()); if (setAuthorizationHeader && secretAvailable) { StringBuilder sb = new StringBuilder(); sb.append("Basic "); try { String data = consumer.getClientId() + ":" + consumer.getClientSecret(); sb.append(Base64Utility.encode(data.getBytes(StandardCharsets.UTF_8))); } catch (Exception ex) { throw new ProcessingException(ex); } accessTokenService.replaceHeader("Authorization", sb.toString()); } else { form.param(OAuthConstants.CLIENT_ID, consumer.getClientId()); if (secretAvailable) { form.param(OAuthConstants.CLIENT_SECRET, consumer.getClientSecret()); } } } else { // in this case the AccessToken service is expected to find a mapping between // the authenticated credentials and the client registration id } Response response = accessTokenService.form(form); Map<String, String> map = null; try { map = new OAuthJSONProvider().readJSONResponse((InputStream)response.getEntity()); } catch (IOException ex) { throw new ResponseProcessingException(response, ex); } if (200 == response.getStatus()) { ClientAccessToken token = fromMapToClientToken(map, defaultTokenType); if (token == null) { throw new OAuthServiceException(OAuthConstants.SERVER_ERROR); } else { return token; } } else if (response.getStatus() >= 400 && map.containsKey(OAuthConstants.ERROR_KEY)) { OAuthError error = new OAuthError(map.get(OAuthConstants.ERROR_KEY), map.get(OAuthConstants.ERROR_DESCRIPTION_KEY)); error.setErrorUri(map.get(OAuthConstants.ERROR_URI_KEY)); throw new OAuthServiceException(error); } throw new OAuthServiceException(OAuthConstants.SERVER_ERROR); } public static ClientAccessToken fromMapToClientToken(Map<String, String> map) { return fromMapToClientToken(map, null); } public static ClientAccessToken fromMapToClientToken(Map<String, String> map, String defaultTokenType) { if (map.containsKey(OAuthConstants.ACCESS_TOKEN)) { String tokenType = map.remove(OAuthConstants.ACCESS_TOKEN_TYPE); if (tokenType == null) { tokenType = defaultTokenType; } if (tokenType != null) { ClientAccessToken token = new ClientAccessToken( tokenType, map.remove(OAuthConstants.ACCESS_TOKEN)); String refreshToken = map.remove(OAuthConstants.REFRESH_TOKEN); if (refreshToken != null) { token.setRefreshToken(refreshToken); } String expiresInStr = map.remove(OAuthConstants.ACCESS_TOKEN_EXPIRES_IN); if (expiresInStr != null) { token.setExpiresIn(Long.valueOf(expiresInStr)); } String issuedAtStr = map.remove(OAuthConstants.ACCESS_TOKEN_ISSUED_AT); token.setIssuedAt(issuedAtStr != null ? Long.valueOf(issuedAtStr) : System.currentTimeMillis() / 1000); String scope = map.remove(OAuthConstants.SCOPE); if (scope != null) { token.setApprovedScope(scope); } token.setParameters(map); return token; } } return null; } /** * Creates OAuth Authorization header with Bearer scheme * @param accessToken the access token * @return the header value */ public static String createAuthorizationHeader(ClientAccessToken accessToken) throws OAuthServiceException { return createAuthorizationHeader(accessToken, null); } /** * Creates OAuth Authorization header with the scheme that * may require an access to the current HTTP request properties * @param accessToken the access token * @param httpProps http request properties, can be null for Bearer tokens * @return the header value */ public static String createAuthorizationHeader(ClientAccessToken accessToken, HttpRequestProperties httpProps) throws OAuthServiceException { StringBuilder sb = new StringBuilder(); appendTokenData(sb, accessToken, httpProps); return sb.toString(); } public static void setAuthorizationHeader(WebClient wc, ClientAccessToken accessToken) { setAuthorizationHeader(wc, accessToken, null); } public static void setAuthorizationHeader(WebClient wc, ClientAccessToken accessToken, String httpVerb) { wc.replaceHeader(HttpHeaders.AUTHORIZATION, createAuthorizationHeader(accessToken, new HttpRequestProperties(wc, httpVerb))); } private static void appendTokenData(StringBuilder sb, ClientAccessToken token, HttpRequestProperties httpProps) throws OAuthServiceException { // this should all be handled by token specific serializers String tokenType = token.getTokenType().toLowerCase(); if (OAuthConstants.BEARER_TOKEN_TYPE.equalsIgnoreCase(tokenType)) { sb.append(OAuthConstants.BEARER_AUTHORIZATION_SCHEME); sb.append(" "); sb.append(token.getTokenKey()); } else if (OAuthConstants.HAWK_TOKEN_TYPE.equalsIgnoreCase(tokenType)) { if (httpProps == null) { throw new IllegalArgumentException("MAC scheme requires HTTP Request properties"); } HawkAuthorizationScheme macAuthData = new HawkAuthorizationScheme(httpProps, token); String macAlgo = token.getParameters().get(OAuthConstants.HAWK_TOKEN_ALGORITHM); String macKey = token.getParameters().get(OAuthConstants.HAWK_TOKEN_KEY); sb.append(macAuthData.toAuthorizationHeader(macAlgo, macKey)); } else { throw new ProcessingException(new OAuthServiceException("Unsupported token type")); } } }