From 1442170a01d56dc2f2789374e11620dbd9e9dda4 Mon Sep 17 00:00:00 2001 From: brasseld Date: Fri, 13 Oct 2017 14:01:47 +0200 Subject: [PATCH] feat(openid): Initial implementation of UserInfo policy Closes gravitee-io/issues#803 --- pom.xml | 151 ++++++++++++++++++ src/assembly/policy-assembly.xml | 59 +++++++ .../openid/userinfo/UserInfoPolicy.java | 119 ++++++++++++++ .../UserInfoPolicyConfiguration.java | 45 ++++++ src/main/resources/plugin.properties | 6 + src/main/resources/schemas/schema-form.json | 20 +++ 6 files changed, 400 insertions(+) create mode 100644 pom.xml create mode 100644 src/assembly/policy-assembly.xml create mode 100644 src/main/java/io/gravitee/policy/openid/userinfo/UserInfoPolicy.java create mode 100644 src/main/java/io/gravitee/policy/openid/userinfo/configuration/UserInfoPolicyConfiguration.java create mode 100644 src/main/resources/plugin.properties create mode 100644 src/main/resources/schemas/schema-form.json diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4c678e1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,151 @@ + + + + 4.0.0 + + io.gravitee.policy + gravitee-policy-openid-connect-userinfo + 1.0.0-SNAPSHOT + + Gravitee.io APIM - Policy - OpenID Connect - UserInfo + Description of the OpenID Connect - UserInfo Gravitee Policy + + + io.gravitee + gravitee-parent + 7 + + + + 1.5.0 + 1.2.0 + 1.0.0 + 1.5.0 + 1.1.0-SNAPSHOT + 2.5.5 + 2.5.3 + + + + + + io.gravitee.gateway + gravitee-gateway-api + ${gravitee-gateway-api.version} + provided + + + + io.gravitee.resource + gravitee-resource-api + ${gravitee-resource-api.version} + provided + + + + io.gravitee.policy + gravitee-policy-api + ${gravitee-policy-api.version} + provided + + + + io.gravitee.common + gravitee-common + ${gravitee-common.version} + provided + + + + io.gravitee.resource + gravitee-resource-oauth2-provider-api + ${gravitee-resource-oauth2-provider-api.version} + provided + + + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + + org.springframework + spring-context + ${spring.version} + provided + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + provided + + + + + junit + junit + ${junit.version} + test + + + + org.mockito + mockito-all + ${mockito.version} + test + + + + + + + src/main/resources + true + + + + + maven-assembly-plugin + ${maven-assembly-plugin.version} + + false + + src/assembly/policy-assembly.xml + + + + + make-policy-assembly + package + + single + + + + + + + + diff --git a/src/assembly/policy-assembly.xml b/src/assembly/policy-assembly.xml new file mode 100644 index 0000000..d75e5ba --- /dev/null +++ b/src/assembly/policy-assembly.xml @@ -0,0 +1,59 @@ + + + policy + + zip + + false + + + + + ${project.build.directory}/${project.build.finalName}.jar + + + + + + + src/main/resources/schemas + schemas + + + + + + ${project.basedir}/src/assembly + lib + + * + + + + + + + + lib + false + + + \ No newline at end of file diff --git a/src/main/java/io/gravitee/policy/openid/userinfo/UserInfoPolicy.java b/src/main/java/io/gravitee/policy/openid/userinfo/UserInfoPolicy.java new file mode 100644 index 0000000..6d5d067 --- /dev/null +++ b/src/main/java/io/gravitee/policy/openid/userinfo/UserInfoPolicy.java @@ -0,0 +1,119 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 io.gravitee.policy.openid.userinfo; + +import io.gravitee.common.http.HttpHeaders; +import io.gravitee.common.http.HttpStatusCode; +import io.gravitee.common.http.MediaType; +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.openid.userinfo.configuration.UserInfoPolicyConfiguration; +import io.gravitee.resource.api.ResourceManager; +import io.gravitee.resource.oauth2.api.OAuth2Resource; +import io.gravitee.resource.oauth2.api.openid.UserInfoResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +/** + * @author David BRASSELY (david.brassely at graviteesource.com) + * @author GraviteeSource Team + */ +public class UserInfoPolicy { + + private final static Logger logger = LoggerFactory.getLogger(UserInfoPolicy.class); + + private static final String BEARER_TYPE = "Bearer"; + + static final String CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN = "oauth.access_token"; + static final String CONTEXT_ATTRIBUTE_OPENID_USERINFO_PAYLOAD = "openid.userinfo.payload"; + private UserInfoPolicyConfiguration userInfoPolicyConfiguration; + + public UserInfoPolicy (UserInfoPolicyConfiguration userInfoPolicyConfiguration) { + this.userInfoPolicyConfiguration = userInfoPolicyConfiguration; + } + + @OnRequest + public void onRequest(Request request, Response response, ExecutionContext executionContext, PolicyChain policyChain) { + logger.debug("Read access_token from request {}", request.id()); + + OAuth2Resource oauth2 = executionContext.getComponent(ResourceManager.class).getResource( + userInfoPolicyConfiguration.getOauthResource(), OAuth2Resource.class); + + if (oauth2 == null) { + policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, + "No OpenID Connect 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(); + 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, + "No OAuth authorization header was supplied")); + return; + } + + 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, + "No OAuth access token was supplied")); + return; + } + + // Set access_token in context + executionContext.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN, accessToken); + + // Validate access token + oauth2.userInfo(accessToken, handleResponse(policyChain, request, response, executionContext)); + } + + private Handler handleResponse(PolicyChain policyChain, Request request, Response response, ExecutionContext executionContext) { + return userInfoResponse -> { + if (userInfoResponse.isSuccess()) { + if (userInfoPolicyConfiguration.isExtractPayload()) { + executionContext.setAttribute(CONTEXT_ATTRIBUTE_OPENID_USERINFO_PAYLOAD, userInfoResponse.getPayload()); + } + + policyChain.doNext(request, response); + } else { + response.headers().add(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE+" realm=gravitee.io " + userInfoResponse.getPayload()); + + if (userInfoResponse.getThrowable() == null) { + policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, + userInfoResponse.getPayload(), MediaType.APPLICATION_JSON)); + } else { + policyChain.failWith(PolicyResult.failure(HttpStatusCode.SERVICE_UNAVAILABLE_503, + "Service Unavailable")); + } + } + }; + } +} diff --git a/src/main/java/io/gravitee/policy/openid/userinfo/configuration/UserInfoPolicyConfiguration.java b/src/main/java/io/gravitee/policy/openid/userinfo/configuration/UserInfoPolicyConfiguration.java new file mode 100644 index 0000000..8630bac --- /dev/null +++ b/src/main/java/io/gravitee/policy/openid/userinfo/configuration/UserInfoPolicyConfiguration.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed 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 io.gravitee.policy.openid.userinfo.configuration; + +import io.gravitee.policy.api.PolicyConfiguration; + +/** + * @author David BRASSELY (david.brassely at graviteesource.com) + * @author GraviteeSource Team + */ +public class UserInfoPolicyConfiguration implements PolicyConfiguration { + + private String oauthResource; + + private boolean extractPayload = false; + + public String getOauthResource() { + return oauthResource; + } + + 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/plugin.properties b/src/main/resources/plugin.properties new file mode 100644 index 0000000..c1bca13 --- /dev/null +++ b/src/main/resources/plugin.properties @@ -0,0 +1,6 @@ +id=policy-openid-userinfo +name=OpenID Connect - UserInfo +version=${project.version} +description=${project.description} +class=io.gravitee.policy.openid.userinfo.UserInfoPolicy +type=policy diff --git a/src/main/resources/schemas/schema-form.json b/src/main/resources/schemas/schema-form.json new file mode 100644 index 0000000..264c8f0 --- /dev/null +++ b/src/main/resources/schemas/schema-form.json @@ -0,0 +1,20 @@ +{ + "type" : "object", + "id" : "urn:jsonschema:io:gravitee:policy:openid:userinfo:configuration:UserInfoPolicyConfiguration", + "properties" : { + "oauthResource" : { + "title": "OpenID Connect resource", + "description": "OpenID Connect resource used to validate token.", + "type" : "string" + }, + "extractPayload" : { + "title": "Extract UserInfo payload", + "description": "Extract UserInfo response and put it into the 'openid.userinfo.payload' context attribute.", + "type" : "boolean", + "default": false + } + }, + "required": [ + "oauthResource" + ] +} \ No newline at end of file