/* * 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.surfnet.oaaas.resource; import com.sun.jersey.api.client.ClientResponse.Status; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.surfnet.oaaas.auth.AbstractAuthenticator; import org.surfnet.oaaas.auth.AbstractUserConsentHandler; import org.surfnet.oaaas.auth.AuthenticationFilter; import org.surfnet.oaaas.auth.OAuth2Validator; import org.surfnet.oaaas.auth.ResourceOwnerAuthenticator; import org.surfnet.oaaas.auth.OAuth2Validator.*; import org.surfnet.oaaas.auth.ValidationResponseException; import org.surfnet.oaaas.auth.principal.AuthenticatedPrincipal; import org.surfnet.oaaas.auth.principal.BasicAuthCredentials; import org.surfnet.oaaas.model.*; import org.surfnet.oaaas.repository.AccessTokenRepository; import org.surfnet.oaaas.repository.AuthorizationRequestRepository; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.*; import javax.ws.rs.core.*; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Arrays; import java.util.UUID; import static org.surfnet.oaaas.auth.OAuth2Validator.*; /** * Resource for handling all calls related to tokens. It adheres to <a * href="http://tools.ietf.org/html/draft-ietf-oauth-v2"> the OAuth spec</a>. * */ @Named @Path("/") public class TokenResource { public static final String BASIC_REALM = "Basic realm=\"OAuth2 Secure\""; public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; @Inject private AuthorizationRequestRepository authorizationRequestRepository; @Inject private AccessTokenRepository accessTokenRepository; @Inject private OAuth2Validator oAuth2Validator; @Inject private ResourceOwnerAuthenticator resourceOwnerAuthenticator; private static final Logger LOG = LoggerFactory.getLogger(TokenResource.class); /** * The "authorization endpoint" as described in <a * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-3.1">Section 3.1</a> of * the OAuth spec. This provides the optional GET support. Access to this endpoint requires * authentication of the requestor (the resource owner) which must be accomplished via a * configured {@link AuthenticationFilter}. * * @param request * the {@link HttpServletRequest} * @return the response */ @GET @Path("/authorize") public Response authorizeCallbackGet(@Context HttpServletRequest request) { return authorizeCallback(request); } /** * Entry point for the authorize call which needs to return an authorization * code or (implicit grant) an access token. Access to this endpoint requires * authentication of the requestor (the resource owner) which must be accomplished via a * configured {@link AuthenticationFilter}. * * @param request * the {@link HttpServletRequest} * @return Response the response */ @POST @Produces(MediaType.TEXT_HTML) @Path("/authorize") public Response authorizeCallback(@Context HttpServletRequest request) { return doProcess(request); } /** * Called after the user has given consent * * @param request * the {@link HttpServletRequest} * @return Response the response */ @POST @Produces(MediaType.TEXT_HTML) @Path("/consent") public Response consentCallback(@Context HttpServletRequest request) { return doProcess(request); } private Response doProcess(HttpServletRequest request) { AuthorizationRequest authReq = findAuthorizationRequest(request); if (authReq == null) { return serverError("Not a valid AbstractAuthenticator.AUTH_STATE on the Request"); } processScopes(authReq, request); if (authReq.getResponseType().equals(OAuth2Validator.IMPLICIT_GRANT_RESPONSE_TYPE)) { AccessToken token = createAccessToken(authReq, true); return sendImplicitGrantResponse(authReq, token); } else { return sendAuthorizationCodeResponse(authReq); } } /* * In the user consent filter the scopes are (possible) set on the Request */ private void processScopes(AuthorizationRequest authReq, HttpServletRequest request) { if (authReq.getClient().isSkipConsent()) { // return the scopes in the authentication request since the requested scopes are stored in the // authorizationRequest. authReq.setGrantedScopes(authReq.getRequestedScopes()); } else { String[] scopes = (String[]) request.getAttribute(AbstractUserConsentHandler.GRANTED_SCOPES); if (!ArrayUtils.isEmpty(scopes)) { authReq.setGrantedScopes(Arrays.asList(scopes)); } else { authReq.setGrantedScopes(null); } } } private AccessToken createAccessToken(AuthorizationRequest request, boolean isImplicitGrant) { Client client = request.getClient(); long expireDuration = client.getExpireDuration(); long expires = (expireDuration == 0L ? 0L : (System.currentTimeMillis() + (1000 * expireDuration))); String refreshToken = (client.isUseRefreshTokens() && !isImplicitGrant) ? getTokenValue(true) : null; AuthenticatedPrincipal principal = request.getPrincipal(); AccessToken token = new AccessToken(getTokenValue(false), principal, client, expires, request.getGrantedScopes(), refreshToken); return accessTokenRepository.save(token); } private AuthorizationRequest findAuthorizationRequest(HttpServletRequest request) { String authState = (String) request.getAttribute(AbstractAuthenticator.AUTH_STATE); return authorizationRequestRepository.findByAuthState(authState); } /** * The "token endpoint" as described in <a * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-3.2">Section 3.2</a> of * the OAuth spec. * * @param authorization the HTTP Basic auth header. * @param formParameters the request parameters * @return the response */ @POST @Path("/token") @Produces(MediaType.APPLICATION_JSON) @Consumes("application/x-www-form-urlencoded") public Response token(@HeaderParam("Authorization") String authorization, final MultivaluedMap<String, String> formParameters) { // Convert incoming parameters into internal form and validate them AccessTokenRequest accessTokenRequest = AccessTokenRequest.fromMultiValuedFormParameters(formParameters); BasicAuthCredentials credentials = BasicAuthCredentials.createCredentialsFromHeader(authorization); ValidationResponse vr = oAuth2Validator.validate(accessTokenRequest, credentials); if (!vr.valid()) { return sendErrorResponse(vr); } // The request looks valid, attempt to process String grantType = accessTokenRequest.getGrantType(); AuthorizationRequest request; try { if (GRANT_TYPE_AUTHORIZATION_CODE.equals(grantType)) { request = authorizationCodeToken(accessTokenRequest); } else if (GRANT_TYPE_REFRESH_TOKEN.equals(grantType)) { request = refreshTokenToken(accessTokenRequest); } else if (GRANT_TYPE_CLIENT_CREDENTIALS.equals(grantType)) { request = clientCredentialToken(accessTokenRequest); } else if (GRANT_TYPE_PASSWORD.equals(grantType)) { request = passwordToken(accessTokenRequest); } else { return sendErrorResponse(ValidationResponse.UNSUPPORTED_GRANT_TYPE); } } catch (ValidationResponseException e) { return sendErrorResponse(e.v); } AccessToken token = createAccessToken(request, false); AccessTokenResponse response = new AccessTokenResponse(token.getToken(), BEARER, token.getExpiresIn(), token.getRefreshToken(), StringUtils.join(token.getScopes(), ' ')); return Response .ok() .entity(response) .cacheControl(cacheControlNoStore()) .header("Pragma", "no-cache") .build(); } private CacheControl cacheControlNoStore() { CacheControl cacheControl = new CacheControl(); cacheControl.setNoStore(true); return cacheControl; } private AuthorizationRequest authorizationCodeToken(AccessTokenRequest accessTokenRequest) { AuthorizationRequest authReq = authorizationRequestRepository.findByAuthorizationCode(accessTokenRequest.getCode()); if (authReq == null) { throw new ValidationResponseException(ValidationResponse.INVALID_GRANT_AUTHORIZATION_CODE); } String uri = accessTokenRequest.getRedirectUri(); if (!authReq.getRedirectUri().equalsIgnoreCase(uri)) { throw new ValidationResponseException(ValidationResponse.REDIRECT_URI_DIFFERENT); } authorizationRequestRepository.delete(authReq); return authReq; } private AuthorizationRequest refreshTokenToken(AccessTokenRequest accessTokenRequest) { AccessToken accessToken = accessTokenRepository.findByRefreshToken(accessTokenRequest.getRefreshToken()); if (accessToken == null) { throw new ValidationResponseException(ValidationResponse.INVALID_GRANT_REFRESH_TOKEN); } AuthorizationRequest request = new AuthorizationRequest(); request.setClient(accessToken.getClient()); request.setPrincipal(accessToken.getPrincipal()); request.setGrantedScopes(accessToken.getScopes()); accessTokenRepository.delete(accessToken); return request; } private AuthorizationRequest clientCredentialToken(AccessTokenRequest accessTokenRequest) { AuthorizationRequest request = new AuthorizationRequest(); request.setClient(accessTokenRequest.getClient()); // We have to construct a AuthenticatedPrincipal on-the-fly as there is only key-secret authentication request.setPrincipal(new AuthenticatedPrincipal(request.getClient().getClientId())); // Get scopes (either from request or the client's default set) request.setGrantedScopes(accessTokenRequest.getScopeList()); return request; } private AuthorizationRequest passwordToken(AccessTokenRequest accessTokenRequest) { // Authenticate the resource owner AuthenticatedPrincipal principal = resourceOwnerAuthenticator.authenticate(accessTokenRequest.getUsername(), accessTokenRequest.getPassword()); if (principal == null) { throw new ValidationResponseException(ValidationResponse.INVALID_GRANT_PASSWORD); } AuthorizationRequest request = new AuthorizationRequest(); request.setClient(accessTokenRequest.getClient()); request.setPrincipal(principal); request.setGrantedScopes(accessTokenRequest.getScopeList()); return request; } private Response sendAuthorizationCodeResponse(AuthorizationRequest authReq) { String uri = authReq.getRedirectUri(); String authorizationCode = getAuthorizationCodeValue(); authReq.setAuthorizationCode(authorizationCode); authorizationRequestRepository.save(authReq); uri = uri + appendQueryMark(uri) + "code=" + authorizationCode + appendStateParameter(authReq); return Response .seeOther(UriBuilder.fromUri(uri).build()) .cacheControl(cacheControlNoStore()) .header("Pragma", "no-cache") .build(); } protected String getTokenValue(boolean isRefreshToken) { return UUID.randomUUID().toString(); } protected String getAuthorizationCodeValue() { return getTokenValue(false); } private Response sendErrorResponse(String error, String description, Status status) { if (status == Status.UNAUTHORIZED) { return Response.status(Status.UNAUTHORIZED).header(WWW_AUTHENTICATE, BASIC_REALM).build(); } return Response.status(status).entity(new ErrorResponse(error, description)).build(); } private Response sendErrorResponse(ValidationResponse response) { return sendErrorResponse(response.getValue(), response.getDescription(), response.getStatus()); } private Response sendImplicitGrantResponse(AuthorizationRequest authReq, AccessToken accessToken) { String uri = authReq.getRedirectUri(); String fragment = String.format("access_token=%s&token_type=bearer&expires_in=%s&scope=%s", accessToken.getToken(), accessToken.getExpiresIn(), StringUtils.join(authReq.getGrantedScopes(), ',')) + appendStateParameter(authReq); if (authReq.getClient().isIncludePrincipal()) { fragment += String.format("&principal=%s", authReq.getPrincipal().getDisplayName()) ; } return Response .seeOther(UriBuilder.fromUri(uri) .fragment(fragment).build()) .cacheControl(cacheControlNoStore()) .header("Pragma", "no-cache") .build(); } private String appendQueryMark(String uri) { return uri.contains("?") ? "&" : "?"; } private String appendStateParameter(AuthorizationRequest authReq) { String state = authReq.getState(); try { return StringUtils.isBlank(state) ? "" : "&state=".concat(URLEncoder.encode(state, "UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private Response serverError(String msg) { LOG.warn(msg); return Response.serverError().build(); } /** * @param authorizationRequestRepository * the authorizationRequestRepository to set */ public void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) { this.authorizationRequestRepository = authorizationRequestRepository; } /** * @param accessTokenRepository * the accessTokenRepository to set */ public void setAccessTokenRepository(AccessTokenRepository accessTokenRepository) { this.accessTokenRepository = accessTokenRepository; } /** * @param oAuth2Validator * the oAuth2Validator to set */ public void setoAuth2Validator(OAuth2Validator oAuth2Validator) { this.oAuth2Validator = oAuth2Validator; } }