/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.ext.oauth; import javax.naming.AuthenticationException; import org.json.JSONException; import org.json.JSONObject; import org.restlet.data.CacheDirective; import org.restlet.data.Form; import org.restlet.data.Status; import org.restlet.ext.json.JsonRepresentation; import org.restlet.ext.oauth.internal.AuthSession; import org.restlet.ext.oauth.internal.AuthSessionTimeoutException; import org.restlet.ext.oauth.internal.Client; import org.restlet.ext.oauth.internal.Client.ClientType; import org.restlet.ext.oauth.internal.ResourceOwnerManager; import org.restlet.ext.oauth.internal.Scopes; import org.restlet.ext.oauth.internal.Token; import org.restlet.representation.Representation; import org.restlet.resource.Post; import org.restlet.resource.ResourceException; import org.restlet.security.User; /** * Server resource used to acquire an OAuth token. A code, or refresh token can * be exchanged for a working token. * * Implements OAuth 2.0 (RFC6749) * * Example. Attach an AccessTokenServerResource * * <pre> * { * @code * public Restlet createInboundRoot(){ * ... * root.attach("/token", AccessTokenServerResource.class); * ... * } * } * </pre> * * @author Shotaro Uchida <fantom@xmaker.mx> * @author Kristoffer Gronowski * * @see <a href="http://tools.ietf.org/html/rfc6749#section-3.2">OAuth 2.0 (3.2. * Token Endpoint)</a> */ public class AccessTokenServerResource extends OAuthServerResource { /** * Executes the 'authorization_code' flow. (4.1.3. Access Token Request) * * @param params * @return * @throws OAuthException * @throws JSONException */ private Representation doAuthCodeFlow(Form params) throws OAuthException, JSONException { // The flow require authenticated client. Client client = getAuthenticatedClient(); if (client == null) { // Use the public client. (4.1.3. Access Token Request) client = getClient(params); } ensureGrantTypeAllowed(client, GrantType.authorization_code); String code = getCode(params); /* * ensure that the authorization code was issued to the authenticated * confidential client, or if the client is public, ensure that the code * was issued to "client_id" in the request, (4.1.3. Access Token * Request) */ AuthSession session = tokens.restoreSession(code); if (!client.getClientId().equals(session.getClientId())) { throw new OAuthException(OAuthError.invalid_grant, "The code was not issued to the client.", null); } try { // Ensure that the session is not timeout. session.updateActivity(); } catch (AuthSessionTimeoutException ex) { throw new OAuthException(OAuthError.invalid_grant, "Code expired.", null); } /* * ensure that the "redirect_uri" parameter is present if the * "redirect_uri" parameter was included in the initial authorization * request as described in Section 4.1.1, and if included ensure that * their values are identical. (4.1.3. Access Token Request) */ if (session.getRedirectionURI().isDynamicConfigured()) { String redirectURI = getRedirectURI(params); if (!redirectURI.equals(session.getRedirectionURI().getURI())) { throw new OAuthException( OAuthError.invalid_grant, "The redirect_uri is not identical to the one included in the initial authorization request.", null); } } Token token = tokens.generateToken(client, session.getScopeOwner(), session.getGrantedScope()); return responseTokenRepresentation(token, session.getRequestedScope()); } /** * Handle errors as described in 5.2 Error Response. * * @param t */ @Override protected void doCatch(Throwable t) { t.printStackTrace(); // FIXME final OAuthException oex = OAuthException.toOAuthException(t); // The authorization server responds with an HTTP 400 (Bad Request) getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST); getResponse().setEntity(responseErrorRepresentation(oex)); // Sets the no-store Cache-Control header addCacheDirective(getResponse(), CacheDirective.noStore()); // TODO: Set Pragma: no-cache } /** * Executes the "client_credentials" flow. (4.4. Client Credentials Grant) * * @param params * @return * @throws OAuthException * @throws JSONException */ private Representation doClientFlow(Form params) throws OAuthException, JSONException { // The flow require authenticated client. Client client = getAuthenticatedClient(); if (client == null || client.getClientType() != ClientType.CONFIDENTIAL) { // The client credentials grant type MUST only be used by // confidential clients. throw new OAuthException( OAuthError.invalid_client, "The client credentials grant type MUST only be used by confidential clients.", null); } ensureGrantTypeAllowed(client, GrantType.client_credentials); String[] requestedScope = getScope(params); // Generate token for the client itself. Token token = tokens.generateToken(client, requestedScope); return responseTokenRepresentation(token, requestedScope); } /** * Executes the "password" flow. (4.3. Resource Owner Password Credentials * Grant) * * @param params * @return * @throws OAuthException * @throws JSONException */ private Representation doPasswordFlow(Form params) throws OAuthException, JSONException { Object users = getContext().getAttributes().get( ResourceOwnerManager.class.getName()); if (users == null) { throw new OAuthException(OAuthError.unsupported_grant_type, "'password' flow is not supported.", null); } // The flow require authenticated client. Client client = getAuthenticatedClient(); if (client == null) { // XXX: 'password' flow MAY use the public client. (3.2.1 Client // Authentication) client = getClient(params); } ensureGrantTypeAllowed(client, GrantType.password); String username = getUsername(params); String password = getPassword(params); String identifier; try { identifier = ((ResourceOwnerManager) users).authenticate(username, password.toCharArray()); } catch (AuthenticationException ex) { throw new OAuthException(OAuthError.invalid_grant, ex.getExplanation(), null); } String[] requestedScope = getScope(params); // Generate token for the resource owner. Token token = tokens.generateToken(client, identifier, requestedScope); return responseTokenRepresentation(token, requestedScope); } /** * Executes the "refresh_token" flow. (6. Refreshing an Access Token) * * @param params * @return * @throws OAuthException * @throws JSONException */ private Representation doRefreshFlow(Form params) throws OAuthException, JSONException { // The flow require authenticated client. Client client = getAuthenticatedClient(); if (client == null) { // XXX: 'refresh' flow MAY use the public client. (3.2.1 Client // Authentication) client = getClient(params); } ensureGrantTypeAllowed(client, GrantType.refresh_token); String refreshToken = getRefreshToken(params); String[] requestedScope = null; String scope = params.getFirstValue(SCOPE); if (scope != null && !scope.isEmpty()) { requestedScope = Scopes.parseScope(scope); } Token token = tokens.refreshToken(client, refreshToken, requestedScope); return responseTokenRepresentation(token, requestedScope); } protected void ensureGrantTypeAllowed(Client client, GrantType grantType) throws OAuthException { if (!client.isGrantTypeAllowed(grantType)) { throw new OAuthException(OAuthError.unauthorized_client, "Unauthorized grant type.", null); } } protected Client getAuthenticatedClient() throws OAuthException { User authenticatedClient = getRequest().getClientInfo().getUser(); if (authenticatedClient == null) { getLogger().warning("Authenticated client_id is missing."); return null; } // XXX: We 'know' the client was authenticated before, 'client' should // not be null. Client client = clients.findById(authenticatedClient.getIdentifier()); getLogger().fine( "Requested by authenticated client " + client.getClientId()); return client; } @Override protected Client getClient(Form params) throws OAuthException { Client client = super.getClient(params); if (client.getClientType() == Client.ClientType.CONFIDENTIAL) { throw new OAuthException(OAuthError.invalid_client, "Unauthenticated confidential client.", null); } else if (client.getClientSecret() != null) { throw new OAuthException(OAuthError.invalid_client, "Unauthenticated public client.", null); } return client; } /** * Get request parameter "code". * * @param params * @return * @throws OAuthException */ protected String getCode(Form params) throws OAuthException { String code = params.getFirstValue(CODE); if (code == null || code.isEmpty()) { throw new OAuthException(OAuthError.invalid_request, "Mandatory parameter code is missing", null); } return code; } /** * Get request parameter "grant_type". * * @param params * @return * @throws OAuthException */ protected GrantType getGrantType(Form params) throws OAuthException { String typeString = params.getFirstValue(GRANT_TYPE); getLogger().info("Type: " + typeString); try { GrantType type = Enum.valueOf(GrantType.class, typeString); getLogger().fine("Found flow - " + type); return type; } catch (IllegalArgumentException iae) { throw new OAuthException(OAuthError.unsupported_grant_type, "Unsupported flow", null); } catch (NullPointerException npe) { throw new OAuthException(OAuthError.invalid_request, "No grant_type parameter found.", null); } } /** * Get request parameter "password". * * @param params * @return * @throws OAuthException */ protected String getPassword(Form params) throws OAuthException { String password = params.getFirstValue(PASSWORD); if (password == null || password.isEmpty()) { throw new OAuthException(OAuthError.invalid_request, "Mandatory parameter password is missing", null); } return password; } /** * Get request parameter "redirect_uri". * * @param params * @return * @throws OAuthException */ protected String getRedirectURI(Form params) throws OAuthException { String redirUri = params.getFirstValue(REDIR_URI); if (redirUri == null || redirUri.isEmpty()) { throw new OAuthException(OAuthError.invalid_request, "Mandatory parameter redirect_uri is missing", null); } return redirUri; } /** * Get request parameter "refresh_token". * * @param params * @return * @throws OAuthException */ protected String getRefreshToken(Form params) throws OAuthException { String token = params.getFirstValue(REFRESH_TOKEN); if (token == null || token.isEmpty()) { throw new OAuthException(OAuthError.invalid_request, "Mandatory parameter refresh_token is missing", null); } return token; } /** * Get request parameter "username". * * @param params * @return * @throws OAuthException */ protected String getUsername(Form params) throws OAuthException { String username = params.getFirstValue(USERNAME); if (username == null || username.isEmpty()) { throw new OAuthException(OAuthError.invalid_request, "Mandatory parameter username is missing", null); } return username; } /** * Handles the {@link Post} request. The client MUST use the HTTP "POST" * method when making access token requests. (3.2. Token Endpoint) * * @param input * HTML form formated token request per oauth-v2 spec. * @return JSON response with token or error. */ @Post("form:json") public Representation requestToken(Representation input) throws OAuthException, JSONException { getLogger().fine("Grant request"); final Form params = new Form(input); final GrantType grantType = getGrantType(params); switch (grantType) { case authorization_code: getLogger().info("Authorization Code Grant"); return doAuthCodeFlow(params); case password: getLogger().info("Resource Owner Password Credentials Grant"); return doPasswordFlow(params); case client_credentials: getLogger().info("Client Credentials Grantt"); return doClientFlow(params); case refresh_token: getLogger().info("Refreshing an Access Token"); return doRefreshFlow(params); default: getLogger().warning("Unsupported flow: " + grantType); throw new OAuthException(OAuthError.unsupported_grant_type, "Flow not supported", null); } } /** * Response JSON document with valid token. The format of the JSON document * is according to 5.1. Successful Response. * * @param token * The token generated by the client. * @param requestedScope * The scope originally requested by the client. * @return The token representation as described in RFC6749 5.1. * @throws ResourceException */ protected Representation responseTokenRepresentation(Token token, String[] requestedScope) throws JSONException { JSONObject response = new JSONObject(); response.put(TOKEN_TYPE, token.getTokenType()); response.put(ACCESS_TOKEN, token.getAccessToken()); response.put(EXPIRES_IN, token.getExpirePeriod()); String refreshToken = token.getRefreshToken(); if (refreshToken != null && !refreshToken.isEmpty()) { response.put(REFRESH_TOKEN, refreshToken); } String[] scope = token.getScope(); if (requestedScope == null || !Scopes.isIdentical(scope, requestedScope)) { /* * OPTIONAL, if identical to the scope requested by the client, * otherwise REQUIRED. (5.1. Successful Response) */ response.put(SCOPE, Scopes.toString(scope)); } /* * The authorization server MUST include the HTTP "Cache-Control" * response header field [RFC2616] with a value of "no-store" in any * response containing tokens, credentials, or other sensitive * information, as well as the "Pragma" response header field [RFC2616] * with a value of "no-cache". (5.1. Successful Response) */ addCacheDirective(getResponse(), CacheDirective.noStore()); // TODO: Set Pragma: no-cache return new JsonRepresentation(response); } }