diff --git a/README.adoc b/README.adoc index 38fbe955..c3d012c9 100644 --- a/README.adoc +++ b/README.adoc @@ -1,40 +1,74 @@ -= Oauth2 Policy += OAuth2 Policy ifdef::env-github[] image:https://ci.gravitee.io/buildStatus/icon?job=gravitee-io/gravitee-policy-oauth2/master["Build status", link="https://ci.gravitee.io/job/gravitee-io/job/gravitee-policy-oauth2/"] image:https://badges.gitter.im/Join Chat.svg["Gitter", link="https://gitter.im/gravitee-io/gravitee-io?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] endif::[] -== Scope +== Phase +[cols="2*", options="header"] |=== -|onRequest |onResponse +^|onRequest +^|onResponse -| X -| +^.^| X +^.^| |=== == Description -Gravitee.io OAuth 2 policy authentication. Check if an OAuth 2 access token is valid during the request process. +The OAuth2 policy checks if an access token is valid during request processing. -If the access token is valid, the request is allowed to proceed, if not the process stops and reject the request. +If the access token is valid, the request is allowed to proceed, if not the process stops and rejects the request. -The access token must be supply in the Authorization HTTP request header : +The access token must be supply in the ```Authorization``` HTTP request header : -[source] +[source, shell] ---- $ curl -H "Authorization: Bearer |accessToken|" \ - http://gravitee.io-gateway/yourApi/yourRestrictedData + http://gateway/api/resource ---- +== Attributes + +|=== +|Name |Description + +.^|oauth.access_token +|Access token extracted from ```Authorization``` HTTP header. + +.^|oauth.payload +|Payload from token endpoint / authorization server. Useful when you want to parse and extract data from it. Only if `extractPayload` is enabled from policy configuration. + +|=== + == Configuration -The policy use the following Gravitee.io OAuth 2.0 resource properties : +This policy use a Gravitee.io resource to access the OAuth2 Authorization Server. + +|=== +|Property |Required |Description |Type| Default + +.^|oauthResource +^.^|X +|The OAuth2 resource used to validate access_token. This must reference a valid Gravitee.io OAuth2 resource. +^.^|string +| + +.^|extractPayload +^.^|- +|When access token is validated, the token endpoint payload is saved under the ```oauth.payload``` context attribute. +^.^|boolean +^.^|false + +|=== + +The OAuth2 resource must be defined with these properties: [source, json] -.Gravitee.io OAuth2 Resource +OAuth2 Resource example: ---- "properties" : { "serverURL" : { @@ -107,6 +141,26 @@ The policy use the following Gravitee.io OAuth 2.0 resource properties : .Sample ---- "oauth2": { - "oauthResource": {} + "oauthResource": "oauth2-resource-name", + "extractPayload": true } ----- \ No newline at end of file +---- + +== Http Status Code + +|=== +|Code |Message + +.^| ```401``` +| In case of: + +* No OAuth authorization server has been configured +* No OAuth authorization header was supplied +* No OAuth access_token was supplied +* Access token can not be validated by authorization server + +.^| ```403``` +| Access token can not be validated because of a technical error with +authorization server. + +|=== \ No newline at end of file diff --git a/pom.xml b/pom.xml index b317fffc..6bc4b5f0 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 1.0.0 1.0.0 1.0.0 - 1.0.0 + 1.1.0-SNAPSHOT 2.5.5 2.0.2 diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index 800c6b33..ca1ed4ef 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -20,46 +20,53 @@ import io.gravitee.gateway.api.ExecutionContext; import io.gravitee.gateway.api.Request; import io.gravitee.gateway.api.Response; +import io.gravitee.gateway.api.handler.Handler; import io.gravitee.policy.api.PolicyChain; import io.gravitee.policy.api.PolicyResult; import io.gravitee.policy.api.annotations.OnRequest; import io.gravitee.policy.oauth2.configuration.OAuth2PolicyConfiguration; import io.gravitee.resource.api.ResourceManager; -import io.gravitee.resource.oauth2.OAuth2Request; import io.gravitee.resource.oauth2.OAuth2Resource; -import io.gravitee.resource.oauth2.configuration.OAuth2ResourceConfiguration; -import org.asynchttpclient.AsyncCompletionHandler; -import org.asynchttpclient.AsyncHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.gravitee.resource.oauth2.OAuth2Response; -import javax.inject.Inject; -import java.util.*; +import java.util.Optional; /** - * @author David BRASSELY (david at gravitee.io) + * @author David BRASSELY (david.brassely at graviteesource.com) + * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) * @author GraviteeSource Team */ public class Oauth2Policy { - private static final Logger LOGGER = LoggerFactory.getLogger(Oauth2Policy.class); - private static final String BEARER_TYPE = "Bearer"; - private static final String OAUTH2_ACCESS_TOKEN = "OAUTH2_ACCESS_TOKEN"; + static final String CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD = "oauth.payload"; + static final String CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN = "oauth.access_token"; - @Inject private OAuth2PolicyConfiguration oAuth2PolicyConfiguration; + public Oauth2Policy (OAuth2PolicyConfiguration oAuth2PolicyConfiguration) { + this.oAuth2PolicyConfiguration = oAuth2PolicyConfiguration; + } + @OnRequest public void onRequest(Request request, Response response, ExecutionContext executionContext, PolicyChain policyChain) { + OAuth2Resource oauth2 = executionContext.getComponent(ResourceManager.class).getResource( + oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class); + + if (oauth2 == null) { + policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, + "No OAuth authorization server has been configured")); + return; + } + if (request.headers() == null || request.headers().get(HttpHeaders.AUTHORIZATION) == null || request.headers().get(HttpHeaders.AUTHORIZATION).isEmpty()) { response.headers().add(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE+" realm=gravitee.io - No OAuth authorization header was supplied"); policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, "No OAuth authorization header was supplied")); return; } - Optional optionalHeaderAccessToken = request.headers().get(HttpHeaders.AUTHORIZATION).stream().filter(h -> h.startsWith("Bearer")).findFirst(); + Optional optionalHeaderAccessToken = request.headers().get(HttpHeaders.AUTHORIZATION).stream().filter(h -> h.startsWith("Bearer")).findFirst(); if (!optionalHeaderAccessToken.isPresent()) { response.headers().add(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE+" realm=gravitee.io - No OAuth authorization header was supplied"); policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, @@ -67,8 +74,7 @@ public void onRequest(Request request, Response response, ExecutionContext execu return; } - String accessToken = extractHeaderToken(optionalHeaderAccessToken.get()); - + String accessToken = optionalHeaderAccessToken.get().substring(BEARER_TYPE.length()).trim(); if (accessToken.isEmpty()) { response.headers().add(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE+" realm=gravitee.io - No OAuth access token was supplied"); policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, @@ -76,64 +82,30 @@ public void onRequest(Request request, Response response, ExecutionContext execu return; } - OAuth2Resource oauth2 = executionContext.getComponent(ResourceManager.class).getResource( - oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class); - - oauth2.validateToken(buildOAuthRequest(oauth2.configuration(), accessToken), responseHandler(policyChain, request, response, executionContext)); - } - - private String extractHeaderToken(String headerAccessToken) { - return headerAccessToken.substring(BEARER_TYPE.length()).trim(); - } - - private OAuth2Request buildOAuthRequest(OAuth2ResourceConfiguration configuration, String accessToken) { - Map> headers = new HashMap<>(); - Map> queryParams = new HashMap<>(); - - OAuth2Request oAuth2Request = new OAuth2Request(); - - oAuth2Request.setUrl(configuration.getServerURL()); - oAuth2Request.setMethod(configuration.getHttpMethod()); - - if (configuration.isSecure()) { - headers.put(configuration.getAuthorizationHeaderName(), - Collections.singletonList(configuration.getAuthorizationScheme().trim() + " " + configuration.getAuthorizationValue())); - } - - if (configuration.isTokenIsSuppliedByQueryParam()) { - queryParams.put(configuration.getTokenQueryParamName(), Collections.singletonList(accessToken)); - } else if (configuration.isTokenIsSuppliedByHttpHeader()) { - headers.put(configuration.getTokenHeaderName(), Collections.singletonList(accessToken)); - } - - oAuth2Request.setHeaders(headers); - oAuth2Request.setQueryParams(queryParams); + // Set access_token in context + executionContext.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN, accessToken); - return oAuth2Request; + // Validate access token + oauth2.validate(accessToken, handleResponse(policyChain, request, response, executionContext)); } - private AsyncHandler responseHandler(PolicyChain policyChain, Request request, Response response, ExecutionContext executionContext) { - return new AsyncCompletionHandler() { + private Handler handleResponse(PolicyChain policyChain, Request request, Response response, ExecutionContext executionContext) { + return oauth2response -> { + if (oauth2response.isSuccess()) { + if (oAuth2PolicyConfiguration.isExtractPayload()) { + executionContext.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD, oauth2response.getPayload()); + } + policyChain.doNext(request, response); + } else { + response.headers().add(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE+" realm=gravitee.io " + oauth2response.getPayload()); - @Override - public Void onCompleted(org.asynchttpclient.Response clientResponse) throws Exception { - if (clientResponse.getStatusCode() == HttpStatusCode.OK_200) { - executionContext.setAttribute(OAUTH2_ACCESS_TOKEN, clientResponse.getResponseBody()); - policyChain.doNext(request, response); - } else { - response.headers().add(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE+" realm=gravitee.io " + clientResponse.getResponseBody()); + if (oauth2response.getThrowable() == null) { policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, - clientResponse.getResponseBody())); + oauth2response.getPayload())); + } else { + policyChain.failWith(PolicyResult.failure(HttpStatusCode.SERVICE_UNAVAILABLE_503, + "Service Unavailable")); } - return null; - } - - @Override - public void onThrowable(Throwable t) { - LOGGER.warn("Unexpected error while invoking remote OAuth2 server", t); - response.headers().add(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE + " realm=gravitee.io " + t.getMessage()); - policyChain.failWith(PolicyResult.failure(HttpStatusCode.SERVICE_UNAVAILABLE_503, - "Service Unavailable")); } }; } diff --git a/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java b/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java index a25b90e9..a6fea0bc 100644 --- a/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java +++ b/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java @@ -18,13 +18,15 @@ import io.gravitee.policy.api.PolicyConfiguration; /** - * @author David BRASSELY (david at gravitee.io) + * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Team */ public class OAuth2PolicyConfiguration implements PolicyConfiguration { private String oauthResource; + private boolean extractPayload = false; + public String getOauthResource() { return oauthResource; } @@ -32,4 +34,12 @@ public String getOauthResource() { public void setOauthResource(String oauthResource) { this.oauthResource = oauthResource; } + + public boolean isExtractPayload() { + return extractPayload; + } + + public void setExtractPayload(boolean extractPayload) { + this.extractPayload = extractPayload; + } } diff --git a/src/main/resources/schemas/urn:jsonschema:io:gravitee:policy:oauth2:configuration:OAuth2PolicyConfiguration.json b/src/main/resources/schemas/urn:jsonschema:io:gravitee:policy:oauth2:configuration:OAuth2PolicyConfiguration.json index 137a451d..fe3bc435 100644 --- a/src/main/resources/schemas/urn:jsonschema:io:gravitee:policy:oauth2:configuration:OAuth2PolicyConfiguration.json +++ b/src/main/resources/schemas/urn:jsonschema:io:gravitee:policy:oauth2:configuration:OAuth2PolicyConfiguration.json @@ -6,6 +6,12 @@ "title": "OAuth2 resource", "description": "OAuth2 resource used to validate token.", "type" : "string" + }, + "extractPayload" : { + "title": "Extract OAuth2 payload", + "description": "Push the token endpoint payload into the 'oauth.payload' context attribute.", + "type" : "boolean", + "default": false } }, "required": [ diff --git a/src/test/java/io/gravitee/policy/oauth2/OAuth2PolicyTest.java b/src/test/java/io/gravitee/policy/oauth2/OAuth2PolicyTest.java index a6a2d623..60c63a30 100644 --- a/src/test/java/io/gravitee/policy/oauth2/OAuth2PolicyTest.java +++ b/src/test/java/io/gravitee/policy/oauth2/OAuth2PolicyTest.java @@ -19,9 +19,12 @@ import io.gravitee.gateway.api.ExecutionContext; import io.gravitee.gateway.api.Request; import io.gravitee.gateway.api.Response; +import io.gravitee.gateway.api.handler.Handler; import io.gravitee.policy.api.PolicyChain; import io.gravitee.policy.api.PolicyResult; -import org.asynchttpclient.AsyncHandler; +import io.gravitee.policy.oauth2.configuration.OAuth2PolicyConfiguration; +import io.gravitee.resource.api.ResourceManager; +import io.gravitee.resource.oauth2.OAuth2Resource; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,13 +35,15 @@ import java.util.UUID; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.internal.verification.VerificationModeFactory.times; /** - * @author Titouan COMPIEGNE (titouan.compiegne at gravitee.io) + * @author David BRASSELY (david.brassely at graviteesource.com) + * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) * @author GraviteeSource Team */ @RunWith(MockitoJUnitRunner.class) @@ -56,16 +61,43 @@ public class OAuth2PolicyTest { @Mock PolicyChain mockPolicychain; + @Mock + ResourceManager resourceManager; + + @Mock + OAuth2Resource oAuth2Resource; + + @Mock + OAuth2PolicyConfiguration oAuth2PolicyConfiguration; + @Before public void init() { initMocks(this); } + @Test + public void shouldFailedIfNoOAuthResourceProvided() { + Oauth2Policy policy = new Oauth2Policy(oAuth2PolicyConfiguration); + + when(mockExecutionContext.getComponent(ResourceManager.class)).thenReturn(resourceManager); + when(resourceManager.getResource(oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class)).thenReturn(oAuth2Resource); + when(mockResponse.headers()).thenReturn(new HttpHeaders()); + + policy.onRequest(mockRequest, mockResponse, mockExecutionContext, mockPolicychain); + + verify(mockPolicychain, times(1)).failWith(any(PolicyResult.class)); + } + @Test public void shouldFailedIfNoAuthorizationHeaderProvided() { - Oauth2Policy policy = new Oauth2Policy(); + Oauth2Policy policy = new Oauth2Policy(oAuth2PolicyConfiguration); + + when(mockExecutionContext.getComponent(ResourceManager.class)).thenReturn(resourceManager); + when(resourceManager.getResource(oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class)).thenReturn(oAuth2Resource); when(mockResponse.headers()).thenReturn(new HttpHeaders()); + policy.onRequest(mockRequest, mockResponse, mockExecutionContext, mockPolicychain); + verify(mockPolicychain, times(1)).failWith(any(PolicyResult.class)); } @@ -78,10 +110,15 @@ public void shouldFailedIfNoAuthorizationHeaderBearerProvided() { } }); - Oauth2Policy policy = new Oauth2Policy(); + Oauth2Policy policy = new Oauth2Policy(oAuth2PolicyConfiguration); + + when(mockExecutionContext.getComponent(ResourceManager.class)).thenReturn(resourceManager); + when(resourceManager.getResource(oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class)).thenReturn(oAuth2Resource); when(mockRequest.headers()).thenReturn(headers); when(mockResponse.headers()).thenReturn(new HttpHeaders()); + policy.onRequest(mockRequest, mockResponse, mockExecutionContext, mockPolicychain); + verify(mockPolicychain, times(1)).failWith(any(PolicyResult.class)); } @@ -94,31 +131,66 @@ public void shouldFailedIfNoAuthorizationAccessTokenBearerIsEmptyProvided() { } }); - Oauth2Policy policy = new Oauth2Policy(); + Oauth2Policy policy = new Oauth2Policy(oAuth2PolicyConfiguration); + + when(mockExecutionContext.getComponent(ResourceManager.class)).thenReturn(resourceManager); + when(resourceManager.getResource(oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class)).thenReturn(oAuth2Resource); when(mockRequest.headers()).thenReturn(headers); when(mockResponse.headers()).thenReturn(new HttpHeaders()); + policy.onRequest(mockRequest, mockResponse, mockExecutionContext, mockPolicychain); + verify(mockPolicychain, times(1)).failWith(any(PolicyResult.class)); } - /* @Test - public void shouldCallOAuthAuthorizationServer() throws Exception { + public void shouldCallOAuthResource() throws Exception { final HttpHeaders headers = new HttpHeaders(); + String bearer = UUID.randomUUID().toString(); + headers.setAll(new HashMap() { { - put("Authorization", "Bearer " + UUID.randomUUID().toString()); + put("Authorization", "Bearer " + bearer); } }); - Oauth2Policy policy = new Oauth2Policy(); + Oauth2Policy policy = new Oauth2Policy(oAuth2PolicyConfiguration); when(mockRequest.headers()).thenReturn(headers); when(mockResponse.headers()).thenReturn(new HttpHeaders()); - when(mockPolicyContext.getComponent(HttpClient.class)).thenReturn(mockHttpClient); - policy.setPolicyContext(mockPolicyContext); + when(mockExecutionContext.getComponent(ResourceManager.class)).thenReturn(resourceManager); + when(oAuth2PolicyConfiguration.getOauthResource()).thenReturn("oauth2"); + when(resourceManager.getResource(oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class)).thenReturn(oAuth2Resource); + policy.onRequest(mockRequest, mockResponse, mockExecutionContext, mockPolicychain); - verify(mockHttpClient, times(1)).validateToken(any(OAuth2Request.class), any(AsyncHandler.class)); + + verify(oAuth2Resource, times(1)).validate(eq(bearer), any(Handler.class)); + verify(mockExecutionContext, times(1)).setAttribute( + eq(Oauth2Policy.CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN), eq(bearer)); + } + + @Test + public void shouldCallOAuthResourceAndHandleResult() throws Exception { + final HttpHeaders headers = new HttpHeaders(); + String bearer = UUID.randomUUID().toString(); + + headers.setAll(new HashMap() { + { + put("Authorization", "Bearer " + bearer); + } + }); + + Oauth2Policy policy = new Oauth2Policy(oAuth2PolicyConfiguration); + when(mockRequest.headers()).thenReturn(headers); + when(mockResponse.headers()).thenReturn(new HttpHeaders()); + when(mockExecutionContext.getComponent(ResourceManager.class)).thenReturn(resourceManager); + when(oAuth2PolicyConfiguration.getOauthResource()).thenReturn("oauth2"); + when(resourceManager.getResource(oAuth2PolicyConfiguration.getOauthResource(), OAuth2Resource.class)).thenReturn(oAuth2Resource); + + policy.onRequest(mockRequest, mockResponse, mockExecutionContext, mockPolicychain); + + verify(oAuth2Resource, times(1)).validate(eq(bearer), any(Handler.class)); + verify(mockExecutionContext, times(1)).setAttribute( + eq(Oauth2Policy.CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN), eq(bearer)); } - */ }