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));
}
- */
}