diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..a9ae158608 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# 4 space - Tab indentation +[*.{java,xml,js,html}] +indent_style = tab +indent_size = 4 diff --git a/.gitignore b/.gitignore index 016a3b8f82..e663c16b8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -local-values.conf target *~ bin @@ -10,3 +9,4 @@ bin .classpath /target .springBeans +nb-configuration.xml diff --git a/.travis.yml b/.travis.yml index dff5f3a5d0..3d619c8263 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,11 @@ language: java +jdk: + - oraclejdk11 +sudo: false + +after_success: + - bash <(curl -s https://codecov.io/bash) + +cache: + directories: + - $HOME/.m2 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..96c6356a0c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +Unreleased: + +*1.3.3*: +- Authorization codes are now longer +- Client/RS can parse the "sub" and "user_id" claims in introspection response +- Database-direct queries for fetching tokens by user (optimization) +- Device flow supports verification_uri_complete (must be turned on) +- Long scopes display properly and are still checkable +- Language system remebers when it can't find a file and stops throwing so many errors +- Index added for refresh tokens +- Updated to Spring Security 4.2.11 +- Updated Spring to 4.3.22 +- Change approve pages to use issuer instead of page context +- Updated oracle database scripts + +*1.3.2*: +- Added changelog +- Set default redirect URI resolver strict matching to true +- Fixed XSS vulnerability on redirect URI display on approval page +- Removed MITRE from copyright +- Disallow unsigned JWTs on client authentication +- Upgraded Nimbus revision +- Added French translation +- Added hooks for custom JWT claims +- Removed "Not Yet Implemented" tag from post-logout redirect URI + +*1.3.1*: +- Added End Session endpoint +- Fixed discovery endpoint +- Downgrade MySQL connector dependency version from developer preview to GA release + +*1.3.0*: +- Added device flow support +- Added PKCE support +- Modularized UI to allow better overlay and extensions +- Modularized data import/export API +- Added software statements to dynamic client registration +- Added assertion processing framework +- Removed ID tokens from storage +- Removed structured scopes + +*1.2.6*: +- Added strict HEART compliance mode diff --git a/LICENSE.txt b/LICENSE.txt index 1a04bce423..0e640e493b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,8 +1,9 @@ -Copyright 2014 The MITRE Corporation - and the MIT Kerberos and Internet Trust Consortium +Copyright 2018 The MIT Internet Trust Consortium + +Portions copyright 2011-2013 The MITRE Corporation Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. +you may not use this project except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/README.md b/README.md index b9da9e863d..610579f550 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # MITREid Connect --- -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.mitre/openid-connect-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.mitre/openid-connect-parent) [![Travis CI](https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server.svg?branch=master)](https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.mitre/openid-connect-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.mitre/openid-connect-parent) [![Travis CI](https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server.svg?branch=master)](https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server) [![Codecov](https://codecov.io/github/mitreid-connect/OpenID-Connect-Java-Spring-Server/coverage.svg?branch=master)](https://codecov.io/github/mitreid-connect/OpenID-Connect-Java-Spring-Server) -This project contains an OpenID Connect reference implementation in Java on the Spring platform, including a functioning [server library](openid-connect-server), [deployable server package](openid-connect-server-webapp), [client (RP) library](openid-connect-client), and general [utility libraries](openid-connect-common). The server can be used as an OpenID Connect Identity Provider as well as a general-purpose OAuth 2.0 Authorization Server. +This project contains a certified OpenID Connect reference implementation in Java on the Spring platform, including a functioning [server library](openid-connect-server), [deployable server package](openid-connect-server-webapp), [client (RP) library](openid-connect-client), and general [utility libraries](openid-connect-common). The server can be used as an OpenID Connect Identity Provider as well as a general-purpose OAuth 2.0 Authorization Server. + +[![OpenID Certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png)](https://openid.net/certification/) More information about the project can be found: @@ -23,9 +25,7 @@ The authors and key contributors of the project include: * [Steve Moore](https://github.com/srmoore) * [Mike Derryberry](https://github.com/mtderryberry) * [William Kim](https://github.com/wikkim) +* [Mark Janssen](https://github.com/praseodym) - - -Copyright ©2014, [The MITRE Corporation](http://www.mitre.org/) - and the [MIT Kerberos and Internet Trust Consortium](http://kit.mit.edu/). Licensed under the Apache 2.0 license, for details see `LICENSE.txt`. +Licensed under the Apache 2.0 license, for details see `LICENSE.txt`. diff --git a/README_zh_CN.md b/README_zh_CN.md new file mode 100644 index 0000000000..4933b36836 --- /dev/null +++ b/README_zh_CN.md @@ -0,0 +1,38 @@ +# MITREid Connect +--- + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.mitre/openid-connect-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.mitre/openid-connect-parent) [![Travis CI](https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server.svg?branch=master)](https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server) + +此项目提供了一个业经认证的、用Java语言构筑于Spring平台之上的OpenID Connect参考实现,包括 [服务器端的实现库](openid-connect-server), [可部署的服务器包](openid-connect-server-webapp), [客户端 (RP) 的库](openid-connect-client), 以及 [工具类库](openid-connect-common)。该服务器可以用做OpenID Connect身份提供者,也可以用做一般意义上的OAuth 2.0授权服务器。 + +[![OpenID认证](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png)](https://openid.net/certification/) + +有关项目的更多信息参见: + +* [项目在GitHub上的主页 (及相关项目)](https://github.com/mitreid-connect/) +* [完整的文档](https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/wiki) +* [Maven文档及Java API](http://mitreid-connect.github.com/) +* [问题(Issue)追踪系统 (用于报告bug及提交支持请求)](https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/issues) +* 项目的邮件列表: `mitreid-connect@mit.edu`, 及其 [在线存档](https://mailman.mit.edu/mailman/listinfo/mitreid-connect). + + +项目的作者及主要贡献者有: + +* [Justin Richer](https://github.com/jricher/) +* [Amanda Anganes](https://github.com/aanganes/) +* [Michael Jett](https://github.com/jumbojett/) +* [Michael Walsh](https://github.com/nemonik/) +* [Steve Moore](https://github.com/srmoore) +* [Mike Derryberry](https://github.com/mtderryberry) +* [William Kim](https://github.com/wikkim) +* [Mark Janssen](https://github.com/praseodym) + + +项目的中文译者: + +* [刘晓曦](https://github.com/liouxiao/) + + + + +版权所有 ©2018 [MIT因特网信任联盟](http://www.mit-trust.org/). 采用Apache 2.0许可证, 详见 `LICENSE.txt`. diff --git a/checkstyle.xml b/checkstyle.xml index 7b96bdff67..06129daddb 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,20 +1,21 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> diff --git a/docs/OAuth2.0_Diagrams.pdf b/docs/OAuth2.0_Diagrams.pdf deleted file mode 100755 index 55b525e92b..0000000000 Binary files a/docs/OAuth2.0_Diagrams.pdf and /dev/null differ diff --git a/docs/OpenID_Connect_Diagrams.pdf b/docs/OpenID_Connect_Diagrams.pdf deleted file mode 100644 index a8da67ec49..0000000000 Binary files a/docs/OpenID_Connect_Diagrams.pdf and /dev/null differ diff --git a/openid-connect-client/README.md b/openid-connect-client/README.md index ba97fe29c1..5bddcdb6ed 100644 --- a/openid-connect-client/README.md +++ b/openid-connect-client/README.md @@ -2,7 +2,7 @@ ## Overview ## -This project contains an OpenID Connect Client implemented as a Spring Security AuthenticationFilter. The client facilitates a user's authentication into the secured application to an OpenID Connect Java Spring Server following the OpenID Connect Standard protocol. +This project contains an OpenID Connect Client implemented as a Spring Security AuthenticationFilter. The client facilitates a user's authentication into the secured application to an OpenID Connect Server following the OpenID Connect standard protocol. ## Configuring ## diff --git a/openid-connect-client/pom.xml b/openid-connect-client/pom.xml index 1bb2c578b0..3fbbd9e5ec 100644 --- a/openid-connect-client/pom.xml +++ b/openid-connect-client/pom.xml @@ -1,75 +1,76 @@ + + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> - 4.0.0 - - openid-connect-parent - org.mitre - 1.1.10-SNAPSHOT - .. - - openid-connect-client - OpenID Connect Client filter for Spring Security - OpenID Connect Client - - - org.mitre - openid-connect-common - 1.1.10-SNAPSHOT - - - jar - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java-version} - ${java-version} - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-sources - - jar - - - - - - + 4.0.0 + + openid-connect-parent + org.mitre + 1.3.5-SNAPSHOT + .. + + openid-connect-client + OpenID Connect Client filter for Spring Security + OpenID Connect Client + + + org.mitre + openid-connect-common + + + jar + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java-version} + ${java-version} + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-sources + + jar + + + + + + diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java index dd35f4edf5..b311a84d9c 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java @@ -1,25 +1,27 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.introspectingfilter; import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.SECRET_BASIC; import java.io.IOException; import java.net.URI; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -27,7 +29,7 @@ import java.util.Set; import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.mitre.oauth2.introspectingfilter.service.IntrospectionAuthorityGranter; import org.mitre.oauth2.introspectingfilter.service.IntrospectionConfigurationService; import org.mitre.oauth2.introspectingfilter.service.impl.SimpleIntrospectionAuthorityGranter; @@ -67,22 +69,49 @@ public class IntrospectingTokenService implements ResourceServerTokenServices { private IntrospectionConfigurationService introspectionConfigurationService; private IntrospectionAuthorityGranter introspectionAuthorityGranter = new SimpleIntrospectionAuthorityGranter(); - private HttpClient httpClient = new SystemDefaultHttpClient(); - private HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); + private int defaultExpireTime = 300000; // 5 minutes in milliseconds + private boolean forceCacheExpireTime = false; // force removal of cached tokens based on default expire time + private boolean cacheNonExpiringTokens = false; + private boolean cacheTokens = true; + + private HttpComponentsClientHttpRequestFactory factory; + + public IntrospectingTokenService() { + this(HttpClientBuilder.create().useSystemProperties().build()); + } + + public IntrospectingTokenService(HttpClient httpClient) { + this.factory = new HttpComponentsClientHttpRequestFactory(httpClient); + } // Inner class to store in the hash map private class TokenCacheObject { OAuth2AccessToken token; OAuth2Authentication auth; + Date cacheExpire; private TokenCacheObject(OAuth2AccessToken token, OAuth2Authentication auth) { this.token = token; this.auth = auth; + + // we don't need to check the cacheTokens values, because this won't actually be added to the cache if cacheTokens is false + // if the token isn't null we use the token expire time + // if forceCacheExpireTime is also true, we also make sure that the token expire time is shorter than the default expire time + if ((this.token.getExpiration() != null) && (!forceCacheExpireTime || (forceCacheExpireTime && (this.token.getExpiration().getTime() - System.currentTimeMillis() <= defaultExpireTime)))) { + this.cacheExpire = this.token.getExpiration(); + } else { // if the token doesn't have an expire time, or if the using forceCacheExpireTime the token expire time is longer than the default, then use the default expire time + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MILLISECOND, defaultExpireTime); + this.cacheExpire = cal.getTime(); + } } } - private Map authCache = new HashMap(); - private static Logger logger = LoggerFactory.getLogger(IntrospectingTokenService.class); + private Map authCache = new HashMap<>(); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(IntrospectingTokenService.class); /** * @return the introspectionConfigurationService @@ -112,12 +141,84 @@ public IntrospectionAuthorityGranter getIntrospectionAuthorityGranter() { return introspectionAuthorityGranter; } - // Check if there is a token and authentication in the cache - // and check if it is not expired. + /** + * get the default cache expire time in milliseconds + * @return + */ + public int getDefaultExpireTime() { + return defaultExpireTime; + } + + /** + * set the default cache expire time in milliseconds + * @param defaultExpireTime + */ + public void setDefaultExpireTime(int defaultExpireTime) { + this.defaultExpireTime = defaultExpireTime; + } + + /** + * check if forcing a cache expire time maximum value + * @return the forceCacheExpireTime setting + */ + public boolean isForceCacheExpireTime() { + return forceCacheExpireTime; + } + + /** + * set forcing a cache expire time maximum value + * @param forceCacheExpireTime + */ + public void setForceCacheExpireTime(boolean forceCacheExpireTime) { + this.forceCacheExpireTime = forceCacheExpireTime; + } + + /** + * Are non-expiring tokens cached using the default cache time + * @return state of cacheNonExpiringTokens + */ + public boolean isCacheNonExpiringTokens() { + return cacheNonExpiringTokens; + } + + /** + * should non-expiring tokens be cached using the default cache timeout + * @param cacheNonExpiringTokens + */ + public void setCacheNonExpiringTokens(boolean cacheNonExpiringTokens) { + this.cacheNonExpiringTokens = cacheNonExpiringTokens; + } + + /** + * Is the service caching tokens, or is it hitting the introspection end point every time + * @return true is caching tokens locally, false hits the introspection end point every time + */ + public boolean isCacheTokens() { + return cacheTokens; + } + + /** + * Configure if the client should cache tokens locally or not + * @param cacheTokens + */ + public void setCacheTokens(boolean cacheTokens) { + this.cacheTokens = cacheTokens; + } + + /** + * Check to see if the introspection end point response for a token has been cached locally + * This call will return the token if it has been cached and is still valid according to + * the cache expire time on the TokenCacheObject. If a cached value has been found but is + * expired, either by default expire times or the token's own expire time, then the token is + * removed from the cache and null is returned. + * @param key is the token to check + * @return the cached TokenCacheObject or null + */ private TokenCacheObject checkCache(String key) { - if (authCache.containsKey(key)) { + if (cacheTokens && authCache.containsKey(key)) { TokenCacheObject tco = authCache.get(key); - if (tco.token.getExpiration().after(new Date())) { + + if (tco != null && tco.cacheExpire != null && tco.cacheExpire.after(new Date())) { return tco; } else { // if the token is expired, don't keep things around. @@ -129,19 +230,27 @@ private TokenCacheObject checkCache(String key) { private OAuth2Request createStoredRequest(final JsonObject token) { String clientId = token.get("client_id").getAsString(); - Set scopes = new HashSet(); + Set scopes = new HashSet<>(); if (token.has("scope")) { scopes.addAll(OAuth2Utils.parseParameterList(token.get("scope").getAsString())); } - Map parameters = new HashMap(); + Map parameters = new HashMap<>(); parameters.put("client_id", clientId); parameters.put("scope", OAuth2Utils.formatParameterList(scopes)); OAuth2Request storedRequest = new OAuth2Request(parameters, clientId, null, true, scopes, null, null, null, null); return storedRequest; } - private Authentication createAuthentication(JsonObject token) { - return new PreAuthenticatedAuthenticationToken(token.get("sub").getAsString(), token, introspectionAuthorityGranter.getAuthorities(token)); + private Authentication createUserAuthentication(JsonObject token) { + JsonElement userId = token.get("user_id"); + if(userId == null) { + userId = token.get("sub"); + if (userId == null) { + return null; + } + } + + return new PreAuthenticatedAuthenticationToken(userId.getAsString(), token, introspectionAuthorityGranter.getAuthorities(token)); } private OAuth2AccessToken createAccessToken(final JsonObject token, final String tokenString) { @@ -149,10 +258,14 @@ private OAuth2AccessToken createAccessToken(final JsonObject token, final String return accessToken; } - // Validate a token string against the introspection endpoint, - // then parse it and store it in the local cache. Return true on - // sucess, false otherwise. - private boolean parseToken(String accessToken) { + /** + * Validate a token string against the introspection endpoint, + * then parse it and store it in the local cache if caching is enabled. + * + * @param accessToken Token to pass to the introspection endpoint + * @return TokenCacheObject containing authentication and token if the token was valid, otherwise null + */ + private TokenCacheObject parseToken(String accessToken) { // find out which URL to ask String introspectionUrl; @@ -162,14 +275,14 @@ private boolean parseToken(String accessToken) { client = introspectionConfigurationService.getClientConfiguration(accessToken); } catch (IllegalArgumentException e) { logger.error("Unable to load introspection URL or client configuration", e); - return false; + return null; } // Use the SpringFramework RestTemplate to send the request to the // endpoint String validatedToken = null; RestTemplate restTemplate; - MultiValueMap form = new LinkedMultiValueMap(); + MultiValueMap form = new LinkedMultiValueMap<>(); final String clientId = client.getClientId(); final String clientSecret = client.getClientSecret(); @@ -199,12 +312,13 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE validatedToken = restTemplate.postForObject(introspectionUrl, form, String.class); } catch (RestClientException rce) { logger.error("validateToken", rce); + return null; } if (validatedToken != null) { // parse the json JsonElement jsonRoot = new JsonParser().parse(validatedToken); if (!jsonRoot.isJsonObject()) { - return false; // didn't get a proper JSON object + return null; // didn't get a proper JSON object } JsonObject tokenResponse = jsonRoot.getAsJsonObject(); @@ -212,29 +326,31 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE if (tokenResponse.get("error") != null) { // report an error? logger.error("Got an error back: " + tokenResponse.get("error") + ", " + tokenResponse.get("error_description")); - return false; + return null; } if (!tokenResponse.get("active").getAsBoolean()) { // non-valid token logger.info("Server returned non-active token"); - return false; + return null; } // create an OAuth2Authentication - OAuth2Authentication auth = new OAuth2Authentication(createStoredRequest(tokenResponse), createAuthentication(tokenResponse)); + OAuth2Authentication auth = new OAuth2Authentication(createStoredRequest(tokenResponse), createUserAuthentication(tokenResponse)); // create an OAuth2AccessToken OAuth2AccessToken token = createAccessToken(tokenResponse, accessToken); - if (token.getExpiration().after(new Date())) { + if (token.getExpiration() == null || token.getExpiration().after(new Date())) { // Store them in the cache - authCache.put(accessToken, new TokenCacheObject(token, auth)); - - return true; + TokenCacheObject tco = new TokenCacheObject(token, auth); + if (cacheTokens && (cacheNonExpiringTokens || token.getExpiration() != null)) { + authCache.put(accessToken, tco); + } + return tco; } } - // If we never put a token and an authentication in the cache... - return false; + // when the token is invalid for whatever reason + return null; } @Override @@ -246,13 +362,9 @@ public OAuth2Authentication loadAuthentication(String accessToken) throws Authen if (cacheAuth != null) { return cacheAuth.auth; } else { - if (parseToken(accessToken)) { - cacheAuth = authCache.get(accessToken); - if (cacheAuth != null && (cacheAuth.token.getExpiration().after(new Date()))) { - return cacheAuth.auth; - } else { - return null; - } + cacheAuth = parseToken(accessToken); + if (cacheAuth != null) { + return cacheAuth.auth; } else { return null; } @@ -268,13 +380,9 @@ public OAuth2AccessToken readAccessToken(String accessToken) { if (cacheAuth != null) { return cacheAuth.token; } else { - if (parseToken(accessToken)) { - cacheAuth = authCache.get(accessToken); - if (cacheAuth != null && (cacheAuth.token.getExpiration().after(new Date()))) { - return cacheAuth.token; - } else { - return null; - } + cacheAuth = parseToken(accessToken); + if (cacheAuth != null) { + return cacheAuth.token; } else { return null; } diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/OAuth2AccessTokenImpl.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/OAuth2AccessTokenImpl.java index 166747953a..723fcc54d0 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/OAuth2AccessTokenImpl.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/OAuth2AccessTokenImpl.java @@ -1,31 +1,27 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.introspectingfilter; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; @@ -37,26 +33,21 @@ public class OAuth2AccessTokenImpl implements OAuth2AccessToken { - private JsonObject token; + private JsonObject introspectionResponse; private String tokenString; - private Set scopes = new HashSet(); + private Set scopes = new HashSet<>(); private Date expireDate; - public OAuth2AccessTokenImpl(JsonObject token, String tokenString) { - this.token = token; + public OAuth2AccessTokenImpl(JsonObject introspectionResponse, String tokenString) { + this.setIntrospectionResponse(introspectionResponse); this.tokenString = tokenString; - if (token.get("scope") != null) { - scopes = Sets.newHashSet(Splitter.on(" ").split(token.get("scope").getAsString())); + if (introspectionResponse.get("scope") != null) { + scopes = Sets.newHashSet(Splitter.on(" ").split(introspectionResponse.get("scope").getAsString())); } - DateFormat dateFormater = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); - if (token.get("exp") != null) { - try { - expireDate = dateFormater.parse(token.get("exp").getAsString()); - } catch (ParseException ex) { - Logger.getLogger(IntrospectingTokenService.class.getName()).log(Level.SEVERE, null, ex); - } + if (introspectionResponse.get("exp") != null) { + expireDate = new Date(introspectionResponse.get("exp").getAsLong() * 1000L); } } @@ -107,4 +98,20 @@ public String getValue() { return tokenString; } + + /** + * @return the token + */ + public JsonObject getIntrospectionResponse() { + return introspectionResponse; + } + + + /** + * @param token the token to set + */ + public void setIntrospectionResponse(JsonObject token) { + this.introspectionResponse = token; + } + } diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionAuthorityGranter.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionAuthorityGranter.java index 4317c7cceb..d514bfbabe 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionAuthorityGranter.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionAuthorityGranter.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.introspectingfilter.service; diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionConfigurationService.java index 6b73029e43..fe85727b5b 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/IntrospectionConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.introspectingfilter.service; diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/JWTParsingIntrospectionConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/JWTParsingIntrospectionConfigurationService.java index ff80006970..b60179f604 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/JWTParsingIntrospectionConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/JWTParsingIntrospectionConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.introspectingfilter.service.impl; @@ -32,11 +33,11 @@ import com.nimbusds.jwt.JWTParser; /** - * + * * Parses the incoming accesstoken as a JWT and determines the issuer based on * the "iss" field inside the JWT. Uses the ServerConfigurationService to determine * the introspection URL for that issuer. - * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/ScopeBasedIntrospectionAuthoritiesGranter.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/ScopeBasedIntrospectionAuthoritiesGranter.java new file mode 100644 index 0000000000..26bc7f11c7 --- /dev/null +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/ScopeBasedIntrospectionAuthoritiesGranter.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.introspectingfilter.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.mitre.oauth2.introspectingfilter.service.IntrospectionAuthorityGranter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.common.util.OAuth2Utils; + +import com.google.gson.JsonObject; + +/** + * @author jricher + * + */ +public class ScopeBasedIntrospectionAuthoritiesGranter implements IntrospectionAuthorityGranter { + + private List authorities = AuthorityUtils.createAuthorityList("ROLE_API"); + + /* (non-Javadoc) + * @see org.mitre.oauth2.introspectingfilter.IntrospectionAuthorityGranter#getAuthorities(net.minidev.json.JSONObject) + */ + @Override + public List getAuthorities(JsonObject introspectionResponse) { + List auth = new ArrayList<>(getAuthorities()); + + if (introspectionResponse.has("scope") && introspectionResponse.get("scope").isJsonPrimitive()) { + String scopeString = introspectionResponse.get("scope").getAsString(); + Set scopes = OAuth2Utils.parseParameterList(scopeString); + for (String scope : scopes) { + auth.add(new SimpleGrantedAuthority("OAUTH_SCOPE_" + scope)); + } + } + + return auth; + } + + /** + * @return the authorities + */ + public List getAuthorities() { + return authorities; + } + + /** + * @param authorities the authorities to set + */ + public void setAuthorities(List authorities) { + this.authorities = authorities; + } + +} diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/SimpleIntrospectionAuthorityGranter.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/SimpleIntrospectionAuthorityGranter.java index 2b38c50d57..45126f2463 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/SimpleIntrospectionAuthorityGranter.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/SimpleIntrospectionAuthorityGranter.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.introspectingfilter.service.impl; @@ -28,9 +29,9 @@ import com.google.gson.JsonObject; /** - * + * * Grants the same set of authorities no matter what's passed in. - * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/StaticIntrospectionConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/StaticIntrospectionConfigurationService.java index dfe8d2eb98..5aa370c415 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/StaticIntrospectionConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/service/impl/StaticIntrospectionConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.introspectingfilter.service.impl; @@ -23,10 +24,10 @@ import org.mitre.oauth2.model.RegisteredClient; /** - * + * * Always provides the (configured) IntrospectionURL and RegisteredClient regardless * of token. Useful for talking to a single, trusted authorization server. - * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/AuthorizationEndpointException.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/AuthorizationEndpointException.java new file mode 100644 index 0000000000..0fe0c7e714 --- /dev/null +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/AuthorizationEndpointException.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.client; + +import org.springframework.security.authentication.AuthenticationServiceException; + +public class AuthorizationEndpointException extends AuthenticationServiceException { + + private static final long serialVersionUID = 6953119789654778380L; + + private String error; + + private String errorDescription; + + private String errorURI; + + public AuthorizationEndpointException(String error, String errorDescription, String errorURI) { + super("Error from Authorization Endpoint: " + error + " " + errorDescription + " " + errorURI); + this.error = error; + this.errorDescription = errorDescription; + this.errorURI = errorURI; + } + + public String getError() { + return error; + } + + public String getErrorDescription() { + return errorDescription; + } + + public String getErrorURI() { + return errorURI; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "AuthorizationEndpointException [error=" + error + ", errorDescription=" + errorDescription + ", errorURI=" + errorURI + "]"; + } +} diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java index 0565332fa1..1d1e810f6a 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java @@ -1,67 +1,78 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client; +import java.text.ParseException; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import org.mitre.openid.connect.model.UserInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; /** - * + * * Simple mapper that adds ROLE_USER to the authorities map for all queries, * plus adds ROLE_ADMIN if the subject and issuer pair are found in the * configurable "admins" set. - * + * * @author jricher - * + * */ -public class NamedAdminAuthoritiesMapper implements GrantedAuthoritiesMapper { +public class NamedAdminAuthoritiesMapper implements OIDCAuthoritiesMapper { + + private static Logger logger = LoggerFactory.getLogger(NamedAdminAuthoritiesMapper.class); private static final SimpleGrantedAuthority ROLE_ADMIN = new SimpleGrantedAuthority("ROLE_ADMIN"); private static final SimpleGrantedAuthority ROLE_USER = new SimpleGrantedAuthority("ROLE_USER"); - private Set admins = new HashSet(); - - private GrantedAuthoritiesMapper chain = new NullAuthoritiesMapper(); + private Set admins = new HashSet<>(); @Override - public Collection mapAuthorities(Collection authorities) { + public Collection mapAuthorities(JWT idToken, UserInfo userInfo) { + + Set out = new HashSet<>(); + try { + JWTClaimsSet claims = idToken.getJWTClaimsSet(); - Set out = new HashSet(); - out.addAll(authorities); + SubjectIssuerGrantedAuthority authority = new SubjectIssuerGrantedAuthority(claims.getSubject(), claims.getIssuer()); + out.add(authority); - for (GrantedAuthority authority : authorities) { if (admins.contains(authority)) { out.add(ROLE_ADMIN); } - } - // everybody's a user by default - out.add(ROLE_USER); + // everybody's a user by default + out.add(ROLE_USER); - return chain.mapAuthorities(out); + } catch (ParseException e) { + logger.error("Unable to parse ID Token inside of authorities mapper (huh?)"); + } + return out; } /** @@ -78,18 +89,4 @@ public void setAdmins(Set admins) { this.admins = admins; } - /** - * @return the chain - */ - public GrantedAuthoritiesMapper getChain() { - return chain; - } - - /** - * @param chain the chain to set - */ - public void setChain(GrantedAuthoritiesMapper chain) { - this.chain = chain; - } - } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationFilter.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationFilter.java index d2c4c46e46..8412525471 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationFilter.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationFilter.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client; import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.PRIVATE_KEY; @@ -23,10 +24,14 @@ import java.io.IOException; import java.math.BigInteger; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.text.ParseException; import java.util.Date; import java.util.Map; +import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -34,10 +39,12 @@ import javax.servlet.http.HttpSession; import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.HttpClientBuilder; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; import org.mitre.jwt.signer.service.impl.JWKSetCacheService; -import org.mitre.jwt.signer.service.impl.SymmetricCacheService; +import org.mitre.jwt.signer.service.impl.SymmetricKeyJWTValidatorCacheService; +import org.mitre.oauth2.model.PKCEAlgorithm; import org.mitre.oauth2.model.RegisteredClient; import org.mitre.openid.connect.client.model.IssuerServiceResponse; import org.mitre.openid.connect.client.service.AuthRequestOptionsService; @@ -47,7 +54,7 @@ import org.mitre.openid.connect.client.service.ServerConfigurationService; import org.mitre.openid.connect.client.service.impl.StaticAuthRequestOptionsService; import org.mitre.openid.connect.config.ServerConfiguration; -import org.mitre.openid.connect.model.OIDCAuthenticationToken; +import org.mitre.openid.connect.model.PendingOIDCAuthenticationToken; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; @@ -59,10 +66,12 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriUtils; import com.google.common.base.Strings; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -71,47 +80,61 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.util.Base64; +import com.nimbusds.jose.util.Base64URL; import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.PlainJWT; -import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; import com.nimbusds.jwt.SignedJWT; /** * OpenID Connect Authentication Filter class - * + * * @author nemonik, jricher - * + * */ public class OIDCAuthenticationFilter extends AbstractAuthenticationProcessingFilter { protected final static String REDIRECT_URI_SESION_VARIABLE = "redirect_uri"; + protected final static String CODE_VERIFIER_SESSION_VARIABLE = "code_verifier"; protected final static String STATE_SESSION_VARIABLE = "state"; protected final static String NONCE_SESSION_VARIABLE = "nonce"; protected final static String ISSUER_SESSION_VARIABLE = "issuer"; - protected static final String TARGET_SESSION_VARIABLE = "target"; + protected final static String TARGET_SESSION_VARIABLE = "target"; protected final static int HTTP_SOCKET_TIMEOUT = 30000; - protected final static String FILTER_PROCESSES_URL = "/openid_connect_login"; + public final static String FILTER_PROCESSES_URL = "/openid_connect_login"; // Allow for time sync issues by having a window of X seconds. private int timeSkewAllowance = 300; - @Autowired + // fetches and caches public keys for servers + @Autowired(required=false) private JWKSetCacheService validationServices; + // creates JWT signer/validators for symmetric keys + @Autowired(required=false) + private SymmetricKeyJWTValidatorCacheService symmetricCacheService; + + // signer based on keypair for this client (for outgoing auth requests) @Autowired(required=false) - private SymmetricCacheService symmetricCacheService; + private JWTSigningAndValidationService authenticationSignerService; @Autowired(required=false) - private JwtSigningAndValidationService authenticationSignerService; + private HttpClient httpClient; - // modular services to build out client filter + /* + * Modular services to build out client filter. + */ + // looks at the request and determines which issuer to use for lookup on the server + private IssuerService issuerService; + // holds server information (auth URI, token URI, etc.), indexed by issuer private ServerConfigurationService servers; + // holds client information (client ID, redirect URI, etc.), indexed by issuer of the server private ClientConfigurationService clients; - private IssuerService issuerService; + // provides extra options to inject into the outbound request private AuthRequestOptionsService authOptions = new StaticAuthRequestOptionsService(); // initialize with an empty set of options + // builds the actual request URI based on input from all other services private AuthRequestUrlBuilder authRequestBuilder; // private helpers to handle target link URLs @@ -123,7 +146,7 @@ public class OIDCAuthenticationFilter extends AbstractAuthenticationProcessingFi /** * OpenIdConnectAuthenticationFilter constructor */ - protected OIDCAuthenticationFilter() { + public OIDCAuthenticationFilter() { super(FILTER_PROCESSES_URL); targetSuccessHandler.passthrough = super.getSuccessHandler(); super.setAuthenticationSuccessHandler(targetSuccessHandler); @@ -140,16 +163,16 @@ public void afterPropertiesSet() { } if (symmetricCacheService == null) { - symmetricCacheService = new SymmetricCacheService(); + symmetricCacheService = new SymmetricKeyJWTValidatorCacheService(); } } /* * This is the main entry point for the filter. - * + * * (non-Javadoc) - * + * * @see org.springframework.security.web.authentication. * AbstractAuthenticationProcessingFilter * #attemptAuthentication(javax.servlet.http.HttpServletRequest, @@ -182,7 +205,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ /** * Initiate an Authorization request - * + * * @param request * The request from which to extract parameters and perform the * authentication @@ -216,8 +239,6 @@ protected void handleAuthorizationRequest(HttpServletRequest request, HttpServle throw new AuthenticationServiceException("No issuer found: " + issuer); } - session.setAttribute(ISSUER_SESSION_VARIABLE, issuer); - ServerConfiguration serverConfig = servers.getServerConfiguration(issuer); if (serverConfig == null) { logger.error("No server configuration found for issuer: " + issuer); @@ -225,6 +246,8 @@ protected void handleAuthorizationRequest(HttpServletRequest request, HttpServle } + session.setAttribute(ISSUER_SESSION_VARIABLE, serverConfig.getIssuer()); + RegisteredClient clientConfig = clients.getClientConfiguration(serverConfig); if (clientConfig == null) { logger.error("No client configuration found for issuer: " + issuer); @@ -234,7 +257,7 @@ protected void handleAuthorizationRequest(HttpServletRequest request, HttpServle String redirectUri = null; if (clientConfig.getRegisteredRedirectUri() != null && clientConfig.getRegisteredRedirectUri().size() == 1) { // if there's a redirect uri configured (and only one), use that - redirectUri = clientConfig.getRegisteredRedirectUri().toArray(new String[] {})[0]; + redirectUri = Iterables.getOnlyElement(clientConfig.getRegisteredRedirectUri()); } else { // otherwise our redirect URI is this current URL, with no query parameters redirectUri = request.getRequestURL().toString(); @@ -249,7 +272,27 @@ protected void handleAuthorizationRequest(HttpServletRequest request, HttpServle Map options = authOptions.getOptions(serverConfig, clientConfig, request); - String authRequest = authRequestBuilder.buildAuthRequestUrl(serverConfig, clientConfig, redirectUri, nonce, state, options); + // if we're using PKCE, handle the challenge here + if (clientConfig.getCodeChallengeMethod() != null) { + String codeVerifier = createCodeVerifier(session); + options.put("code_challenge_method", clientConfig.getCodeChallengeMethod().getName()); + if (clientConfig.getCodeChallengeMethod().equals(PKCEAlgorithm.plain)) { + options.put("code_challenge", codeVerifier); + } else if (clientConfig.getCodeChallengeMethod().equals(PKCEAlgorithm.S256)) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + String hash = Base64URL.encode(digest.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII))).toString(); + options.put("code_challenge", hash); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + + } + } + + String authRequest = authRequestBuilder.buildAuthRequestUrl(serverConfig, clientConfig, redirectUri, nonce, state, options, issResp.getLoginHint()); logger.debug("Auth Request: " + authRequest); @@ -272,11 +315,9 @@ protected Authentication handleAuthorizationCodeResponse(HttpServletRequest requ // check for state, if it doesn't match we bail early String storedState = getStoredState(session); - if (!Strings.isNullOrEmpty(storedState)) { - String state = request.getParameter("state"); - if (!storedState.equals(state)) { - throw new AuthenticationServiceException("State parameter mismatch on return. Expected " + storedState + " got " + state); - } + String requestState = request.getParameter("state"); + if (storedState == null || !storedState.equals(requestState)) { + throw new AuthenticationServiceException("State parameter mismatch on return. Expected " + storedState + " got " + requestState); } // look up the issuer that we set out to talk to @@ -286,9 +327,15 @@ protected Authentication handleAuthorizationCodeResponse(HttpServletRequest requ ServerConfiguration serverConfig = servers.getServerConfiguration(issuer); final RegisteredClient clientConfig = clients.getClientConfiguration(serverConfig); - MultiValueMap form = new LinkedMultiValueMap(); + MultiValueMap form = new LinkedMultiValueMap<>(); form.add("grant_type", "authorization_code"); form.add("code", authorizationCode); + form.setAll(authOptions.getTokenOptions(serverConfig, clientConfig, request)); + + String codeVerifier = getStoredCodeVerifier(session); + if (codeVerifier != null) { + form.add("code_verifier", codeVerifier); + } String redirectUri = getStoredSessionString(session, REDIRECT_URI_SESION_VARIABLE); if (redirectUri != null) { @@ -296,9 +343,15 @@ protected Authentication handleAuthorizationCodeResponse(HttpServletRequest requ } // Handle Token Endpoint interaction - HttpClient httpClient = new SystemDefaultHttpClient(); - httpClient.getParams().setParameter("http.socket.timeout", new Integer(httpSocketTimeout)); + if(httpClient == null) { + httpClient = HttpClientBuilder.create() + .useSystemProperties() + .setDefaultRequestConfig(RequestConfig.custom() + .setSocketTimeout(httpSocketTimeout) + .build()) + .build(); + } HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); @@ -312,9 +365,9 @@ protected Authentication handleAuthorizationCodeResponse(HttpServletRequest requ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException { ClientHttpRequest httpRequest = super.createRequest(url, method); httpRequest.getHeaders().add("Authorization", - String.format("Basic %s", Base64.encode(String.format("%s:%s", clientConfig.getClientId(), clientConfig.getClientSecret())) )); - - + String.format("Basic %s", Base64.encode(String.format("%s:%s", + UriUtils.encodePathSegment(clientConfig.getClientId(), "UTF-8"), + UriUtils.encodePathSegment(clientConfig.getClientSecret(), "UTF-8"))))); return httpRequest; } @@ -327,13 +380,13 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE // do a symmetric secret signed JWT for auth - JwtSigningAndValidationService signer = null; + JWTSigningAndValidationService signer = null; JWSAlgorithm alg = clientConfig.getTokenEndpointAuthSigningAlg(); if (SECRET_JWT.equals(clientConfig.getTokenEndpointAuthMethod()) && - (alg.equals(JWSAlgorithm.HS256) - || alg.equals(JWSAlgorithm.HS384) - || alg.equals(JWSAlgorithm.HS512))) { + (JWSAlgorithm.HS256.equals(alg) + || JWSAlgorithm.HS384.equals(alg) + || JWSAlgorithm.HS512.equals(alg))) { // generate one based on client secret signer = symmetricCacheService.getSymmetricValidtor(clientConfig.getClient()); @@ -342,27 +395,35 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE // needs to be wired in to the bean signer = authenticationSignerService; + + if (alg == null) { + alg = authenticationSignerService.getDefaultSigningAlgorithm(); + } } if (signer == null) { throw new AuthenticationServiceException("Couldn't find required signer service for use with private key auth."); } - JWTClaimsSet claimsSet = new JWTClaimsSet(); + JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder(); - claimsSet.setIssuer(clientConfig.getClientId()); - claimsSet.setSubject(clientConfig.getClientId()); - claimsSet.setAudience(Lists.newArrayList(serverConfig.getTokenEndpointUri())); + claimsSet.issuer(clientConfig.getClientId()); + claimsSet.subject(clientConfig.getClientId()); + claimsSet.audience(Lists.newArrayList(serverConfig.getTokenEndpointUri())); + claimsSet.jwtID(UUID.randomUUID().toString()); // TODO: make this configurable Date exp = new Date(System.currentTimeMillis() + (60 * 1000)); // auth good for 60 seconds - claimsSet.setExpirationTime(exp); + claimsSet.expirationTime(exp); Date now = new Date(System.currentTimeMillis()); - claimsSet.setIssueTime(now); - claimsSet.setNotBeforeTime(now); + claimsSet.issueTime(now); + claimsSet.notBeforeTime(now); - SignedJWT jwt = new SignedJWT(new JWSHeader(alg), claimsSet); + JWSHeader header = new JWSHeader(alg, null, null, null, null, null, null, null, null, null, + signer.getDefaultSignerKeyId(), + null, null); + SignedJWT jwt = new SignedJWT(header, claimsSet.build()); signer.signJwt(jwt, alg); @@ -383,15 +444,13 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE try { jsonString = restTemplate.postForObject(serverConfig.getTokenEndpointUri(), form, String.class); - } catch (HttpClientErrorException httpClientErrorException) { + } catch (RestClientException e) { // Handle error - logger.error("Token Endpoint error response: " - + httpClientErrorException.getStatusText() + " : " - + httpClientErrorException.getMessage()); + logger.error("Token Endpoint error response: " + e.getMessage()); - throw new AuthenticationServiceException("Unable to obtain Access Token: " + httpClientErrorException.getMessage()); + throw new AuthenticationServiceException("Unable to obtain Access Token: " + e.getMessage()); } logger.debug("from TokenEndpoint jsonString = " + jsonString); @@ -444,45 +503,45 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE JWT idToken = JWTParser.parse(idTokenValue); // validate our ID Token over a number of tests - ReadOnlyJWTClaimsSet idClaims = idToken.getJWTClaimsSet(); + JWTClaimsSet idClaims = idToken.getJWTClaimsSet(); // check the signature - JwtSigningAndValidationService jwtValidator = null; + JWTSigningAndValidationService jwtValidator = null; Algorithm tokenAlg = idToken.getHeader().getAlgorithm(); - + Algorithm clientAlg = clientConfig.getIdTokenSignedResponseAlg(); - + if (clientAlg != null) { if (!clientAlg.equals(tokenAlg)) { throw new AuthenticationServiceException("Token algorithm " + tokenAlg + " does not match expected algorithm " + clientAlg); } } - + if (idToken instanceof PlainJWT) { - + if (clientAlg == null) { throw new AuthenticationServiceException("Unsigned ID tokens can only be used if explicitly configured in client."); } - - if (tokenAlg != null && !tokenAlg.equals(JWSAlgorithm.NONE)) { + + if (tokenAlg != null && !tokenAlg.equals(Algorithm.NONE)) { throw new AuthenticationServiceException("Unsigned token received, expected signature with " + tokenAlg); } } else if (idToken instanceof SignedJWT) { - + SignedJWT signedIdToken = (SignedJWT)idToken; - + if (tokenAlg.equals(JWSAlgorithm.HS256) - || tokenAlg.equals(JWSAlgorithm.HS384) - || tokenAlg.equals(JWSAlgorithm.HS512)) { - + || tokenAlg.equals(JWSAlgorithm.HS384) + || tokenAlg.equals(JWSAlgorithm.HS512)) { + // generate one based on client secret jwtValidator = symmetricCacheService.getSymmetricValidtor(clientConfig.getClient()); } else { // otherwise load from the server's public key jwtValidator = validationServices.getValidator(serverConfig.getJwksUri()); } - + if (jwtValidator != null) { if(!jwtValidator.validateSignature(signedIdToken)) { throw new AuthenticationServiceException("Signature validation failed"); @@ -556,13 +615,11 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE + "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + "."); } - // pull the subject (user id) out as a claim on the id_token - - String userId = idClaims.getSubject(); + // construct an PendingOIDCAuthenticationToken and return a Authentication object w/the userId and the idToken - // construct an OIDCAuthenticationToken and return a Authentication object w/the userId and the idToken - - OIDCAuthenticationToken token = new OIDCAuthenticationToken(userId, idClaims.getIssuer(), serverConfig, idTokenValue, accessTokenValue, refreshTokenValue); + PendingOIDCAuthenticationToken token = new PendingOIDCAuthenticationToken(idClaims.getSubject(), idClaims.getIssuer(), + serverConfig, + idToken, accessTokenValue, refreshTokenValue); Authentication authentication = this.getAuthenticationManager().authenticate(token); @@ -578,7 +635,7 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE /** * Handle Authorization Endpoint error - * + * * @param request * The request from which to extract parameters and handle the * error @@ -593,7 +650,7 @@ protected void handleError(HttpServletRequest request, HttpServletResponse respo String errorDescription = request.getParameter("error_description"); String errorURI = request.getParameter("error_uri"); - throw new AuthenticationServiceException("Error from Authorization Endpoint: " + error + " " + errorDescription + " " + errorURI); + throw new AuthorizationEndpointException(error, errorDescription, errorURI); } /** @@ -653,6 +710,26 @@ protected static String getStoredState(HttpSession session) { return getStoredSessionString(session, STATE_SESSION_VARIABLE); } + /** + * Create a random code challenge and store it in the session + * @param session + * @return + */ + protected static String createCodeVerifier(HttpSession session) { + String challenge = new BigInteger(50, new SecureRandom()).toString(16); + session.setAttribute(CODE_VERIFIER_SESSION_VARIABLE, challenge); + return challenge; + } + + /** + * Retrieve the stored challenge from our session + * @param session + * @return + */ + protected static String getStoredCodeVerifier(HttpSession session) { + return getStoredSessionString(session, CODE_VERIFIER_SESSION_VARIABLE); + } + @Override public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) { @@ -685,7 +762,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, if (!Strings.isNullOrEmpty(target)) { session.removeAttribute(TARGET_SESSION_VARIABLE); - target = deepLinkFilter.filter(target); + if (deepLinkFilter != null) { + target = deepLinkFilter.filter(target); + } response.sendRedirect(target); } else { @@ -795,11 +874,11 @@ public void setAuthRequestOptionsService(AuthRequestOptionsService authOptions) this.authOptions = authOptions; } - public SymmetricCacheService getSymmetricCacheService() { + public SymmetricKeyJWTValidatorCacheService getSymmetricCacheService() { return symmetricCacheService; } - public void setSymmetricCacheService(SymmetricCacheService symmetricCacheService) { + public void setSymmetricCacheService(SymmetricKeyJWTValidatorCacheService symmetricCacheService) { this.symmetricCacheService = symmetricCacheService; } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java index 68a5056bfd..b43b649df2 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java @@ -1,100 +1,128 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client; import java.util.Collection; import org.mitre.openid.connect.model.OIDCAuthenticationToken; +import org.mitre.openid.connect.model.PendingOIDCAuthenticationToken; import org.mitre.openid.connect.model.UserInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import com.google.common.base.Strings; -import com.google.common.collect.Lists; +import com.nimbusds.jwt.JWT; /** - * @author nemonik - * + * @author nemonik, Justin Richer + * */ public class OIDCAuthenticationProvider implements AuthenticationProvider { + private static Logger logger = LoggerFactory.getLogger(OIDCAuthenticationProvider.class); + private UserInfoFetcher userInfoFetcher = new UserInfoFetcher(); - private GrantedAuthoritiesMapper authoritiesMapper = new NamedAdminAuthoritiesMapper(); + private OIDCAuthoritiesMapper authoritiesMapper = new NamedAdminAuthoritiesMapper(); /* * (non-Javadoc) - * + * * @see org.springframework.security.authentication.AuthenticationProvider# * authenticate(org.springframework.security.core.Authentication) */ @Override - public Authentication authenticate(final Authentication authentication) - throws AuthenticationException { + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { if (!supports(authentication.getClass())) { return null; } - if (authentication instanceof OIDCAuthenticationToken) { + if (authentication instanceof PendingOIDCAuthenticationToken) { - OIDCAuthenticationToken token = (OIDCAuthenticationToken) authentication; + PendingOIDCAuthenticationToken token = (PendingOIDCAuthenticationToken) authentication; - Collection authorities = Lists.newArrayList(new SubjectIssuerGrantedAuthority(token.getSub(), token.getIssuer())); + // get the ID Token value out + JWT idToken = token.getIdToken(); + // load the user info if we can UserInfo userInfo = userInfoFetcher.loadUserInfo(token); if (userInfo == null) { - // TODO: user Info not found -- error? + // user info not found -- could be an error, could be fine } else { + // if we found userinfo, double check it if (!Strings.isNullOrEmpty(userInfo.getSub()) && !userInfo.getSub().equals(token.getSub())) { // the userinfo came back and the user_id fields don't match what was in the id_token throw new UsernameNotFoundException("user_id mismatch between id_token and user_info call: " + token.getSub() + " / " + userInfo.getSub()); } } - return new OIDCAuthenticationToken(token.getSub(), - token.getIssuer(), - userInfo, authoritiesMapper.mapAuthorities(authorities), - token.getIdTokenValue(), token.getAccessTokenValue(), token.getRefreshTokenValue()); + return createAuthenticationToken(token, authoritiesMapper.mapAuthorities(idToken, userInfo), userInfo); } return null; } + /** + * Override this function to return a different kind of Authentication, processes the authorities differently, + * or do post-processing based on the UserInfo object. + * + * @param token + * @param authorities + * @param userInfo + * @return + */ + protected Authentication createAuthenticationToken(PendingOIDCAuthenticationToken token, Collection authorities, UserInfo userInfo) { + return new OIDCAuthenticationToken(token.getSub(), + token.getIssuer(), + userInfo, authorities, + token.getIdToken(), token.getAccessTokenValue(), token.getRefreshTokenValue()); + } + + /** + * @param userInfoFetcher + */ + public void setUserInfoFetcher(UserInfoFetcher userInfoFetcher) { + this.userInfoFetcher = userInfoFetcher; + } + /** * @param authoritiesMapper */ - public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { + public void setAuthoritiesMapper(OIDCAuthoritiesMapper authoritiesMapper) { this.authoritiesMapper = authoritiesMapper; } /* * (non-Javadoc) - * + * * @see * org.springframework.security.authentication.AuthenticationProvider#supports * (java.lang.Class) */ @Override public boolean supports(Class authentication) { - return OIDCAuthenticationToken.class.isAssignableFrom(authentication); + return PendingOIDCAuthenticationToken.class.isAssignableFrom(authentication); } } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthoritiesMapper.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthoritiesMapper.java new file mode 100644 index 0000000000..0ee1d1c66e --- /dev/null +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthoritiesMapper.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.client; + +import java.util.Collection; + +import org.mitre.openid.connect.model.UserInfo; +import org.springframework.security.core.GrantedAuthority; + +import com.nimbusds.jwt.JWT; + +/** + * @author jricher + * + */ +public interface OIDCAuthoritiesMapper { + + /** + * @param idToken the ID Token (parsed as a JWT, cannot be @null) + * @param userInfo userInfo of the current user (could be @null) + * @return the set of authorities to map to this user + */ + Collection mapAuthorities(JWT idToken, UserInfo userInfo); + +} diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/StaticPrefixTargetLinkURIChecker.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/StaticPrefixTargetLinkURIChecker.java index 9df9954594..b7725bbe07 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/StaticPrefixTargetLinkURIChecker.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/StaticPrefixTargetLinkURIChecker.java @@ -1,6 +1,5 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +18,7 @@ /** * Simple target URI checker, checks whether the string in question starts * with a configured prefix. Returns "/" if the match fails. - * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/SubjectIssuerGrantedAuthority.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/SubjectIssuerGrantedAuthority.java index 01ffff0774..9d4c85c511 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/SubjectIssuerGrantedAuthority.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/SubjectIssuerGrantedAuthority.java @@ -1,21 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * * 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 org.mitre.openid.connect.client; @@ -24,9 +23,9 @@ import com.google.common.base.Strings; /** - * + * * Simple authority representing a user at an issuer. - * + * * @author jricher * */ @@ -51,9 +50,9 @@ public SubjectIssuerGrantedAuthority(String subject, String issuer) { /** * Returns a string formed by concatenating the subject with the issuer, separated by _ and prepended with OIDC_ - * + * * For example, the user "bob" from issuer "http://id.example.com/" would return the authority string of: - * + * * OIDC_bob_http://id.example.com/ */ @Override diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/TargetLinkURIChecker.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/TargetLinkURIChecker.java index 04f5e7d63e..1fca0bfeb1 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/TargetLinkURIChecker.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/TargetLinkURIChecker.java @@ -1,6 +1,5 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +19,7 @@ public interface TargetLinkURIChecker { /** * Check the parameter to make sure that it's a valid deep-link into this application. - * + * * @param target * @return */ diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/UserInfoFetcher.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/UserInfoFetcher.java index 5b6caf7267..5b755617bb 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/UserInfoFetcher.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/UserInfoFetcher.java @@ -1,31 +1,35 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.apache.http.client.HttpClient; import org.apache.http.client.utils.URIBuilder; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.mitre.openid.connect.config.ServerConfiguration; import org.mitre.openid.connect.config.ServerConfiguration.UserInfoTokenMethod; import org.mitre.openid.connect.model.DefaultUserInfo; -import org.mitre.openid.connect.model.OIDCAuthenticationToken; +import org.mitre.openid.connect.model.PendingOIDCAuthenticationToken; import org.mitre.openid.connect.model.UserInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,44 +41,76 @@ import org.springframework.web.client.RestTemplate; import com.google.common.base.Strings; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.gson.JsonObject; import com.google.gson.JsonParser; /** - * Utility class to fetch userinfo from the userinfo endpoint, if available. + * Utility class to fetch userinfo from the userinfo endpoint, if available. Caches the results. * @author jricher * */ public class UserInfoFetcher { - private Logger logger = LoggerFactory.getLogger(UserInfoFetcher.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(UserInfoFetcher.class); - public UserInfo loadUserInfo(final OIDCAuthenticationToken token) { + private LoadingCache cache; - ServerConfiguration serverConfiguration = token.getServerConfiguration(); + public UserInfoFetcher() { + this(HttpClientBuilder.create().useSystemProperties().build()); + } - if (serverConfiguration == null) { - logger.warn("No server configuration found."); + public UserInfoFetcher(HttpClient httpClient) { + cache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) // expires 1 hour after fetch + .maximumSize(100) + .build(new UserInfoLoader(httpClient)); + } + + public UserInfo loadUserInfo(final PendingOIDCAuthenticationToken token) { + try { + return cache.get(token); + } catch (UncheckedExecutionException | ExecutionException e) { + logger.warn("Couldn't load User Info from token: " + e.getMessage()); return null; } - if (Strings.isNullOrEmpty(serverConfiguration.getUserInfoUri())) { - logger.warn("No userinfo endpoint, not fetching."); - return null; + } + + + private class UserInfoLoader extends CacheLoader { + private HttpComponentsClientHttpRequestFactory factory; + + UserInfoLoader(HttpClient httpClient) { + this.factory = new HttpComponentsClientHttpRequestFactory(httpClient); } - try { - - // if we got this far, try to actually get the userinfo - HttpClient httpClient = new SystemDefaultHttpClient(); - - HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); - + @Override + public UserInfo load(final PendingOIDCAuthenticationToken token) throws URISyntaxException { + + ServerConfiguration serverConfiguration = token.getServerConfiguration(); + + if (serverConfiguration == null) { + logger.warn("No server configuration found."); + return null; + } + + if (Strings.isNullOrEmpty(serverConfiguration.getUserInfoUri())) { + logger.warn("No userinfo endpoint, not fetching."); + return null; + } + String userInfoString = null; - + if (serverConfiguration.getUserInfoTokenMethod() == null || serverConfiguration.getUserInfoTokenMethod().equals(UserInfoTokenMethod.HEADER)) { RestTemplate restTemplate = new RestTemplate(factory) { - + @Override protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException { ClientHttpRequest httpRequest = super.createRequest(url, method); @@ -82,19 +118,19 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE return httpRequest; } }; - + userInfoString = restTemplate.getForObject(serverConfiguration.getUserInfoUri(), String.class); - + } else if (serverConfiguration.getUserInfoTokenMethod().equals(UserInfoTokenMethod.FORM)) { - MultiValueMap form = new LinkedMultiValueMap(); + MultiValueMap form = new LinkedMultiValueMap<>(); form.add("access_token", token.getAccessTokenValue()); - + RestTemplate restTemplate = new RestTemplate(factory); userInfoString = restTemplate.postForObject(serverConfiguration.getUserInfoUri(), form, String.class); } else if (serverConfiguration.getUserInfoTokenMethod().equals(UserInfoTokenMethod.QUERY)) { URIBuilder builder = new URIBuilder(serverConfiguration.getUserInfoUri()); builder.setParameter("access_token", token.getAccessTokenValue()); - + RestTemplate restTemplate = new RestTemplate(factory); userInfoString = restTemplate.getForObject(builder.toString(), String.class); } @@ -103,19 +139,19 @@ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOE if (!Strings.isNullOrEmpty(userInfoString)) { JsonObject userInfoJson = new JsonParser().parse(userInfoString).getAsJsonObject(); - - UserInfo userInfo = DefaultUserInfo.fromJson(userInfoJson); + + UserInfo userInfo = fromJson(userInfoJson); return userInfo; } else { - // didn't get anything, return null - return null; + // didn't get anything throw exception + throw new IllegalArgumentException("Unable to load user info"); } - } catch (Exception e) { - logger.warn("Error fetching userinfo", e); - return null; - } + } } + protected UserInfo fromJson(JsonObject userInfoJson) { + return DefaultUserInfo.fromJson(userInfoJson); + } } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisher.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisher.java index 673a558832..dfd0eea85c 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisher.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisher.java @@ -1,26 +1,27 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.keypublisher; import java.util.Map; import java.util.UUID; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; -import org.mitre.openid.connect.view.JwkKeyListView; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.openid.connect.view.JWKSetView; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -37,13 +38,13 @@ */ public class ClientKeyPublisher implements BeanDefinitionRegistryPostProcessor { - private JwtSigningAndValidationService signingAndValidationService; + private JWTSigningAndValidationService signingAndValidationService; private String jwkPublishUrl; private BeanDefinitionRegistry registry; - private String jwkViewName = "jwkKeyList"; + private String jwkViewName = JWKSetView.VIEWNAME; /** * If the jwkPublishUrl field is set on this bean, set up a listener on that URL to publish keys. @@ -61,13 +62,13 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) clientKeyMapping.addPropertyValue("jwkPublishUrl", getJwkPublishUrl()); // randomize view name to make sure it doesn't conflict with local views - jwkViewName = "jwkKeyList-" + UUID.randomUUID().toString(); + jwkViewName = JWKSetView.VIEWNAME + "-" + UUID.randomUUID().toString(); viewResolver.addPropertyValue("jwkViewName", jwkViewName); // view bean - BeanDefinitionBuilder jwkView = BeanDefinitionBuilder.rootBeanDefinition(JwkKeyListView.class); - registry.registerBeanDefinition("jwkKeyList", jwkView.getBeanDefinition()); - viewResolver.addPropertyReference("jwk", "jwkKeyList"); + BeanDefinitionBuilder jwkView = BeanDefinitionBuilder.rootBeanDefinition(JWKSetView.class); + registry.registerBeanDefinition(JWKSetView.VIEWNAME, jwkView.getBeanDefinition()); + viewResolver.addPropertyReference("jwk", JWKSetView.VIEWNAME); } registry.registerBeanDefinition("clientKeyMapping", clientKeyMapping.getBeanDefinition()); @@ -114,14 +115,14 @@ public void setJwkPublishUrl(String jwkPublishUrl) { /** * @return the signingAndValidationService */ - public JwtSigningAndValidationService getSigningAndValidationService() { + public JWTSigningAndValidationService getSigningAndValidationService() { return signingAndValidationService; } /** * @param signingAndValidationService the signingAndValidationService to set */ - public void setSigningAndValidationService(JwtSigningAndValidationService signingAndValidationService) { + public void setSigningAndValidationService(JWTSigningAndValidationService signingAndValidationService) { this.signingAndValidationService = signingAndValidationService; } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisherMapping.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisherMapping.java index 55e59c503c..e601f3c271 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisherMapping.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/ClientKeyPublisherMapping.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.keypublisher; diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/JwkViewResolver.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/JwkViewResolver.java index d5999a9ce7..30ebdd63fd 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/JwkViewResolver.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/keypublisher/JwkViewResolver.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.keypublisher; @@ -26,9 +27,9 @@ import org.springframework.web.servlet.ViewResolver; /** - * + * * Simple view resolver to map JWK view names to appropriate beans - * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/model/IssuerServiceResponse.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/model/IssuerServiceResponse.java index 1355a71eff..e8de16a13d 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/model/IssuerServiceResponse.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/model/IssuerServiceResponse.java @@ -1,28 +1,29 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.model; /** - * + * * Data container to facilitate returns from the IssuerService API. - * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestOptionsService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestOptionsService.java index ffdb85dae8..73a8d377f1 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestOptionsService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestOptionsService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.client.service; @@ -27,15 +28,34 @@ import org.mitre.openid.connect.config.ServerConfiguration; /** - * - * This service provides any extra options that need to be passed to the authentication request. + * + * This service provides any extra options that need to be passed to the authentication request, + * either through the authorization endpoint (getOptions) or the token endpoint (getTokenOptions). * These options may depend on the server configuration, client configuration, or HTTP request. - * + * * @author jricher * */ public interface AuthRequestOptionsService { + /** + * The set of options needed at the authorization endpoint. + * + * @param server + * @param client + * @param request + * @return + */ public Map getOptions(ServerConfiguration server, RegisteredClient client, HttpServletRequest request); + /** + * The set of options needed at the token endpoint. + * + * @param server + * @param client + * @param request + * @return + */ + public Map getTokenOptions(ServerConfiguration server, RegisteredClient client, HttpServletRequest request); + } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestUrlBuilder.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestUrlBuilder.java index 6a89a5fd50..14eb8a09ca 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestUrlBuilder.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/AuthRequestUrlBuilder.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service; @@ -25,6 +26,8 @@ import org.mitre.openid.connect.config.ServerConfiguration; /** + * Builds a URL string to the IdP's authorization endpoint. + * * @author jricher * */ @@ -36,8 +39,9 @@ public interface AuthRequestUrlBuilder { * @param redirectUri * @param nonce * @param state + * @param loginHint * @return */ - public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options); + public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options, String loginHint); } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ClientConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ClientConfigurationService.java index e6c8eb8f62..6444376b3e 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ClientConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ClientConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service; diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/IssuerService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/IssuerService.java index b631d34a66..7e4e527024 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/IssuerService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/IssuerService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service; @@ -24,9 +25,9 @@ import org.mitre.openid.connect.client.model.IssuerServiceResponse; /** - * + * * Gets an issuer for the given request. Might do dynamic discovery, or might be statically configured. - * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/RegisteredClientService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/RegisteredClientService.java index 3b56b376f6..0ca59bc103 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/RegisteredClientService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/RegisteredClientService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service; diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ServerConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ServerConfigurationService.java index c8cf10f860..44613fdc5e 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ServerConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/ServerConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service; diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicRegistrationClientConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicRegistrationClientConfigurationService.java index 320d32cd87..2c32fd8fd9 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicRegistrationClientConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicRegistrationClientConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -24,7 +25,7 @@ import java.util.concurrent.ExecutionException; import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.mitre.oauth2.model.RegisteredClient; import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor; import org.mitre.openid.connect.client.service.ClientConfigurationService; @@ -39,6 +40,8 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import com.google.common.cache.CacheBuilder; @@ -55,7 +58,10 @@ */ public class DynamicRegistrationClientConfigurationService implements ClientConfigurationService { - private static Logger logger = LoggerFactory.getLogger(DynamicServerConfigurationService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DynamicRegistrationClientConfigurationService.class); private LoadingCache clients; @@ -63,29 +69,30 @@ public class DynamicRegistrationClientConfigurationService implements ClientConf private RegisteredClient template; - private Set whitelist = new HashSet(); - private Set blacklist = new HashSet(); + private Set whitelist = new HashSet<>(); + private Set blacklist = new HashSet<>(); public DynamicRegistrationClientConfigurationService() { - clients = CacheBuilder.newBuilder().build(new DynamicClientRegistrationLoader()); + this(HttpClientBuilder.create().useSystemProperties().build()); + } + + public DynamicRegistrationClientConfigurationService(HttpClient httpClient) { + clients = CacheBuilder.newBuilder().build(new DynamicClientRegistrationLoader(httpClient)); } @Override public RegisteredClient getClientConfiguration(ServerConfiguration issuer) { try { - if (!whitelist.isEmpty() && !whitelist.contains(issuer)) { + if (!whitelist.isEmpty() && !whitelist.contains(issuer.getIssuer())) { throw new AuthenticationServiceException("Whitelist was nonempty, issuer was not in whitelist: " + issuer); } - if (blacklist.contains(issuer)) { + if (blacklist.contains(issuer.getIssuer())) { throw new AuthenticationServiceException("Issuer was in blacklist: " + issuer); } return clients.get(issuer); - } catch (UncheckedExecutionException ue) { - logger.warn("Unable to get client configuration", ue); - return null; - } catch (ExecutionException e) { + } catch (UncheckedExecutionException | ExecutionException e) { logger.warn("Unable to get client configuration", e); return null; } @@ -158,18 +165,25 @@ public void setBlacklist(Set blacklist) { /** * Loader class that fetches the client information. - * + * * If a client has been registered (ie, it's known to the RegisteredClientService), then this * will fetch the client's configuration from the server. - * + * * @author jricher * */ public class DynamicClientRegistrationLoader extends CacheLoader { - private HttpClient httpClient = new SystemDefaultHttpClient(); - private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + private HttpComponentsClientHttpRequestFactory httpFactory; private Gson gson = new Gson(); // note that this doesn't serialize nulls by default + public DynamicClientRegistrationLoader() { + this(HttpClientBuilder.create().useSystemProperties().build()); + } + + public DynamicClientRegistrationLoader(HttpClient httpClient) { + this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + } + @Override public RegisteredClient load(ServerConfiguration serverConfig) throws Exception { RestTemplate restTemplate = new RestTemplate(httpFactory); @@ -186,34 +200,41 @@ public RegisteredClient load(ServerConfiguration serverConfig) throws Exception headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Lists.newArrayList(MediaType.APPLICATION_JSON)); - HttpEntity entity = new HttpEntity(serializedClient, headers); + HttpEntity entity = new HttpEntity<>(serializedClient, headers); - String registered = restTemplate.postForObject(serverConfig.getRegistrationEndpointUri(), entity, String.class); - // TODO: handle HTTP errors + try { + String registered = restTemplate.postForObject(serverConfig.getRegistrationEndpointUri(), entity, String.class); - RegisteredClient client = ClientDetailsEntityJsonProcessor.parseRegistered(registered); + RegisteredClient client = ClientDetailsEntityJsonProcessor.parseRegistered(registered); - // save this client for later - registeredClientService.save(serverConfig.getIssuer(), client); + // save this client for later + registeredClientService.save(serverConfig.getIssuer(), client); - return client; + return client; + } catch (RestClientException rce) { + throw new InvalidClientException("Error registering client with server"); + } } else { if (knownClient.getClientId() == null) { - + // load this client's information from the server HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, knownClient.getRegistrationAccessToken())); headers.setAccept(Lists.newArrayList(MediaType.APPLICATION_JSON)); - - HttpEntity entity = new HttpEntity(headers); - - String registered = restTemplate.exchange(knownClient.getRegistrationClientUri(), HttpMethod.GET, entity, String.class).getBody(); - // TODO: handle HTTP errors - - RegisteredClient client = ClientDetailsEntityJsonProcessor.parseRegistered(registered); - - return client; + + HttpEntity entity = new HttpEntity<>(headers); + + try { + String registered = restTemplate.exchange(knownClient.getRegistrationClientUri(), HttpMethod.GET, entity, String.class).getBody(); + // TODO: handle HTTP errors + + RegisteredClient client = ClientDetailsEntityJsonProcessor.parseRegistered(registered); + + return client; + } catch (RestClientException rce) { + throw new InvalidClientException("Error loading previously registered client information from server"); + } } else { // it's got a client ID from the store, don't bother trying to load it return knownClient; diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java index 0e99633ca2..5f451c2dcb 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java @@ -1,37 +1,38 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.mitre.discovery.util.JsonUtils.getAsBoolean; -import static org.mitre.discovery.util.JsonUtils.getAsEncryptionMethodList; -import static org.mitre.discovery.util.JsonUtils.getAsJweAlgorithmList; -import static org.mitre.discovery.util.JsonUtils.getAsJwsAlgorithmList; -import static org.mitre.discovery.util.JsonUtils.getAsString; -import static org.mitre.discovery.util.JsonUtils.getAsStringList; +import static org.mitre.util.JsonUtils.getAsBoolean; +import static org.mitre.util.JsonUtils.getAsEncryptionMethodList; +import static org.mitre.util.JsonUtils.getAsJweAlgorithmList; +import static org.mitre.util.JsonUtils.getAsJwsAlgorithmList; +import static org.mitre.util.JsonUtils.getAsString; +import static org.mitre.util.JsonUtils.getAsStringList; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutionException; import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.mitre.openid.connect.client.service.ServerConfigurationService; import org.mitre.openid.connect.config.ServerConfiguration; import org.slf4j.Logger; @@ -49,25 +50,32 @@ import com.google.gson.JsonParser; /** - * + * * Dynamically fetches OpenID Connect server configurations based on the issuer. Caches the server configurations. - * + * * @author jricher * */ public class DynamicServerConfigurationService implements ServerConfigurationService { - private static Logger logger = LoggerFactory.getLogger(DynamicServerConfigurationService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DynamicServerConfigurationService.class); // map of issuer -> server configuration, loaded dynamically from service discovery private LoadingCache servers; - private Set whitelist = new HashSet(); - private Set blacklist = new HashSet(); + private Set whitelist = new HashSet<>(); + private Set blacklist = new HashSet<>(); public DynamicServerConfigurationService() { + this(HttpClientBuilder.create().useSystemProperties().build()); + } + + public DynamicServerConfigurationService(HttpClient httpClient) { // initialize the cache - servers = CacheBuilder.newBuilder().build(new OpenIDConnectServiceConfigurationFetcher()); + servers = CacheBuilder.newBuilder().build(new OpenIDConnectServiceConfigurationFetcher(httpClient)); } /** @@ -111,11 +119,8 @@ public ServerConfiguration getServerConfiguration(String issuer) { } return servers.get(issuer); - } catch (UncheckedExecutionException ue) { - logger.warn("Couldn't load configuration for " + issuer, ue); - return null; - } catch (ExecutionException e) { - logger.warn("Couldn't load configuration for " + issuer, e); + } catch (UncheckedExecutionException | ExecutionException e) { + logger.warn("Couldn't load configuration for " + issuer + ": " + e); return null; } @@ -126,10 +131,13 @@ public ServerConfiguration getServerConfiguration(String issuer) { * */ private class OpenIDConnectServiceConfigurationFetcher extends CacheLoader { - private HttpClient httpClient = new SystemDefaultHttpClient(); - private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + private HttpComponentsClientHttpRequestFactory httpFactory; private JsonParser parser = new JsonParser(); + OpenIDConnectServiceConfigurationFetcher(HttpClient httpClient) { + this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + } + @Override public ServerConfiguration load(String issuer) throws Exception { RestTemplate restTemplate = new RestTemplate(httpFactory); @@ -154,7 +162,7 @@ public ServerConfiguration load(String issuer) throws Exception { } if (!issuer.equals(o.get("issuer").getAsString())) { - throw new IllegalStateException("Discovered issuers didn't match, expected " + issuer + " got " + o.get("issuer").getAsString()); + logger.info("Issuer used for discover was " + issuer + " but final issuer is " + o.get("issuer").getAsString()); } conf.setIssuer(o.get("issuer").getAsString()); diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/EncryptedAuthRequestUrlBuilder.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/EncryptedAuthRequestUrlBuilder.java index be83e70ac9..cad7d7399a 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/EncryptedAuthRequestUrlBuilder.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/EncryptedAuthRequestUrlBuilder.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.client.service.impl; @@ -24,7 +25,7 @@ import java.util.Map.Entry; import org.apache.http.client.utils.URIBuilder; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; import org.mitre.jwt.signer.service.impl.JWKSetCacheService; import org.mitre.oauth2.model.RegisteredClient; import org.mitre.openid.connect.client.service.AuthRequestUrlBuilder; @@ -32,6 +33,7 @@ import org.springframework.security.authentication.AuthenticationServiceException; import com.google.common.base.Joiner; +import com.google.common.base.Strings; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWEHeader; @@ -54,33 +56,38 @@ public class EncryptedAuthRequestUrlBuilder implements AuthRequestUrlBuilder { * @see org.mitre.openid.connect.client.service.AuthRequestUrlBuilder#buildAuthRequestUrl(org.mitre.openid.connect.config.ServerConfiguration, org.mitre.oauth2.model.RegisteredClient, java.lang.String, java.lang.String, java.lang.String, java.util.Map) */ @Override - public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options) { + public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options, String loginHint) { // create our signed JWT for the request object - JWTClaimsSet claims = new JWTClaimsSet(); + JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder(); //set parameters to JwtClaims - claims.setClaim("response_type", "code"); - claims.setClaim("client_id", clientConfig.getClientId()); - claims.setClaim("scope", Joiner.on(" ").join(clientConfig.getScope())); + claims.claim("response_type", "code"); + claims.claim("client_id", clientConfig.getClientId()); + claims.claim("scope", Joiner.on(" ").join(clientConfig.getScope())); // build our redirect URI - claims.setClaim("redirect_uri", redirectUri); + claims.claim("redirect_uri", redirectUri); // this comes back in the id token - claims.setClaim("nonce", nonce); + claims.claim("nonce", nonce); // this comes back in the auth request return - claims.setClaim("state", state); + claims.claim("state", state); // Optional parameters for (Entry option : options.entrySet()) { - claims.setClaim(option.getKey(), option.getValue()); + claims.claim(option.getKey(), option.getValue()); + } + + // if there's a login hint, send it + if (!Strings.isNullOrEmpty(loginHint)) { + claims.claim("login_hint", loginHint); } - EncryptedJWT jwt = new EncryptedJWT(new JWEHeader(alg, enc), claims); + EncryptedJWT jwt = new EncryptedJWT(new JWEHeader(alg, enc), claims.build()); - JwtEncryptionAndDecryptionService encryptor = encrypterService.getEncrypter(serverConfig.getJwksUri()); + JWTEncryptionAndDecryptionService encryptor = encrypterService.getEncrypter(serverConfig.getJwksUri()); encryptor.encryptJwt(jwt); diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridClientConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridClientConfigurationService.java index 940263c325..16fed24ed3 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridClientConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridClientConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -31,12 +32,12 @@ * Houses both a static client configuration and a dynamic client configuration * service in one object. Checks the static service first, then falls through to * the dynamic service. - * + * * Provides configuration passthrough for the template, registered client service, whitelist, * and blacklist for the dynamic service, and to the static service's client map. - * + * * @author jricher - * + * */ public class HybridClientConfigurationService implements ClientConfigurationService { diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridIssuerService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridIssuerService.java index 47e581bee8..816f03698e 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridIssuerService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridIssuerService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,16 +27,48 @@ import com.google.common.collect.Sets; /** - * + * * Issuer service that tries to parse input from the inputs from a third-party * account chooser service (if possible), but falls back to webfinger discovery * if not. - * + * * @author jricher * */ public class HybridIssuerService implements IssuerService { + /** + * @return + * @see org.mitre.openid.connect.client.service.impl.ThirdPartyIssuerService#getAccountChooserUrl() + */ + public String getAccountChooserUrl() { + return thirdPartyIssuerService.getAccountChooserUrl(); + } + + /** + * @param accountChooserUrl + * @see org.mitre.openid.connect.client.service.impl.ThirdPartyIssuerService#setAccountChooserUrl(java.lang.String) + */ + public void setAccountChooserUrl(String accountChooserUrl) { + thirdPartyIssuerService.setAccountChooserUrl(accountChooserUrl); + } + + /** + * @return + * @see org.mitre.openid.connect.client.service.impl.WebfingerIssuerService#isForceHttps() + */ + public boolean isForceHttps() { + return webfingerIssuerService.isForceHttps(); + } + + /** + * @param forceHttps + * @see org.mitre.openid.connect.client.service.impl.WebfingerIssuerService#setForceHttps(boolean) + */ + public void setForceHttps(boolean forceHttps) { + webfingerIssuerService.setForceHttps(forceHttps); + } + private ThirdPartyIssuerService thirdPartyIssuerService = new ThirdPartyIssuerService(); private WebfingerIssuerService webfingerIssuerService = new WebfingerIssuerService(); diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridServerConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridServerConfigurationService.java index 2a01339e07..cf519442ca 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridServerConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/HybridServerConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -29,11 +30,11 @@ * Houses both a static server configuration and a dynamic server configuration * service in one object. Checks the static service first, then falls through to * the dynamic service. - * + * * Provides configuration passthrough to the dynamic service's whitelist and blacklist, * and to the static service's server map. - * - * + * + * * @author jricher * */ diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/InMemoryRegisteredClientService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/InMemoryRegisteredClientService.java index 315fe47fda..6be9eca8e7 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/InMemoryRegisteredClientService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/InMemoryRegisteredClientService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -31,7 +32,7 @@ */ public class InMemoryRegisteredClientService implements RegisteredClientService { - private Map clients = new HashMap(); + private Map clients = new HashMap<>(); /* (non-Javadoc) * @see org.mitre.openid.connect.client.service.RegisteredClientService#getByIssuer(java.lang.String) diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/JsonFileRegisteredClientService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/JsonFileRegisteredClientService.java index 68cfd10080..de69bb8f3e 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/JsonFileRegisteredClientService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/JsonFileRegisteredClientService.java @@ -1,26 +1,26 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -50,27 +50,30 @@ */ public class JsonFileRegisteredClientService implements RegisteredClientService { - private static Logger logger = LoggerFactory.getLogger(JsonFileRegisteredClientService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JsonFileRegisteredClientService.class); private Gson gson = new GsonBuilder() - .registerTypeAdapter(RegisteredClient.class, new JsonSerializer() { - @Override - public JsonElement serialize(RegisteredClient src, Type typeOfSrc, JsonSerializationContext context) { - return ClientDetailsEntityJsonProcessor.serialize(src); - } - }) - .registerTypeAdapter(RegisteredClient.class, new JsonDeserializer() { - @Override - public RegisteredClient deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return ClientDetailsEntityJsonProcessor.parseRegistered(json); - } - }) - .setPrettyPrinting() - .create(); + .registerTypeAdapter(RegisteredClient.class, new JsonSerializer() { + @Override + public JsonElement serialize(RegisteredClient src, Type typeOfSrc, JsonSerializationContext context) { + return ClientDetailsEntityJsonProcessor.serialize(src); + } + }) + .registerTypeAdapter(RegisteredClient.class, new JsonDeserializer() { + @Override + public RegisteredClient deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return ClientDetailsEntityJsonProcessor.parseRegistered(json); + } + }) + .setPrettyPrinting() + .create(); private File file; - private Map clients = new HashMap(); + private Map clients = new HashMap<>(); public JsonFileRegisteredClientService(String filename) { this.file = new File(filename); @@ -97,6 +100,7 @@ public void save(String issuer, RegisteredClient client) { /** * Sync the map of clients out to disk. */ + @SuppressWarnings("serial") private void write() { try { if (!file.exists()) { @@ -110,8 +114,6 @@ private void write() { out.close(); - } catch (FileNotFoundException e) { - logger.error("Could not write to output file", e); } catch (IOException e) { logger.error("Could not write to output file", e); } @@ -120,6 +122,7 @@ private void write() { /** * Load the map in from disk. */ + @SuppressWarnings("serial") private void load() { try { if (!file.exists()) { @@ -132,8 +135,6 @@ private void load() { in.close(); - } catch (FileNotFoundException e) { - logger.error("Could not read from input file", e); } catch (IOException e) { logger.error("Could not read from input file", e); } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/PlainAuthRequestUrlBuilder.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/PlainAuthRequestUrlBuilder.java index 7c4cccc776..86ecece0ef 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/PlainAuthRequestUrlBuilder.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/PlainAuthRequestUrlBuilder.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -30,11 +31,12 @@ import org.springframework.security.authentication.AuthenticationServiceException; import com.google.common.base.Joiner; +import com.google.common.base.Strings; /** - * + * * Builds an auth request redirect URI with normal query parameters. - * + * * @author jricher * */ @@ -44,7 +46,7 @@ public class PlainAuthRequestUrlBuilder implements AuthRequestUrlBuilder { * @see org.mitre.openid.connect.client.service.AuthRequestUrlBuilder#buildAuthRequest(javax.servlet.http.HttpServletRequest, org.mitre.openid.connect.config.ServerConfiguration, org.springframework.security.oauth2.provider.ClientDetails) */ @Override - public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options) { + public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options, String loginHint) { try { URIBuilder uriBuilder = new URIBuilder(serverConfig.getAuthorizationEndpointUri()); @@ -63,6 +65,11 @@ public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredCl uriBuilder.addParameter(option.getKey(), option.getValue()); } + // if there's a login hint, send it + if (!Strings.isNullOrEmpty(loginHint)) { + uriBuilder.addParameter("login_hint", loginHint); + } + return uriBuilder.build().toString(); } catch (URISyntaxException e) { diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/SignedAuthRequestUrlBuilder.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/SignedAuthRequestUrlBuilder.java index 78fcd9bd7e..604a72a391 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/SignedAuthRequestUrlBuilder.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/SignedAuthRequestUrlBuilder.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -24,13 +25,15 @@ import java.util.Map.Entry; import org.apache.http.client.utils.URIBuilder; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; import org.mitre.oauth2.model.RegisteredClient; import org.mitre.openid.connect.client.service.AuthRequestUrlBuilder; import org.mitre.openid.connect.config.ServerConfiguration; import org.springframework.security.authentication.AuthenticationServiceException; import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; @@ -41,41 +44,49 @@ */ public class SignedAuthRequestUrlBuilder implements AuthRequestUrlBuilder { - private JwtSigningAndValidationService signingAndValidationService; + private JWTSigningAndValidationService signingAndValidationService; /* (non-Javadoc) * @see org.mitre.openid.connect.client.service.AuthRequestUrlBuilder#buildAuthRequestUrl(org.mitre.openid.connect.config.ServerConfiguration, org.springframework.security.oauth2.provider.ClientDetails, java.lang.String, java.lang.String, java.lang.String) */ @Override - public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options) { + public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredClient clientConfig, String redirectUri, String nonce, String state, Map options, String loginHint) { // create our signed JWT for the request object - JWTClaimsSet claims = new JWTClaimsSet(); + JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder(); //set parameters to JwtClaims - claims.setClaim("response_type", "code"); - claims.setClaim("client_id", clientConfig.getClientId()); - claims.setClaim("scope", Joiner.on(" ").join(clientConfig.getScope())); + claims.claim("response_type", "code"); + claims.claim("client_id", clientConfig.getClientId()); + claims.claim("scope", Joiner.on(" ").join(clientConfig.getScope())); // build our redirect URI - claims.setClaim("redirect_uri", redirectUri); + claims.claim("redirect_uri", redirectUri); // this comes back in the id token - claims.setClaim("nonce", nonce); + claims.claim("nonce", nonce); // this comes back in the auth request return - claims.setClaim("state", state); + claims.claim("state", state); // Optional parameters for (Entry option : options.entrySet()) { - claims.setClaim(option.getKey(), option.getValue()); + claims.claim(option.getKey(), option.getValue()); } + // if there's a login hint, send it + if (!Strings.isNullOrEmpty(loginHint)) { + claims.claim("login_hint", loginHint); + } + JWSAlgorithm alg = clientConfig.getRequestObjectSigningAlg(); + if (alg == null) { + alg = signingAndValidationService.getDefaultSigningAlgorithm(); + } - SignedJWT jwt = new SignedJWT(new JWSHeader(signingAndValidationService.getDefaultSigningAlgorithm()), claims); + SignedJWT jwt = new SignedJWT(new JWSHeader(alg), claims.build()); - signingAndValidationService.signJwt(jwt); + signingAndValidationService.signJwt(jwt, alg); try { URIBuilder uriBuilder = new URIBuilder(serverConfig.getAuthorizationEndpointUri()); @@ -91,14 +102,14 @@ public String buildAuthRequestUrl(ServerConfiguration serverConfig, RegisteredCl /** * @return the signingAndValidationService */ - public JwtSigningAndValidationService getSigningAndValidationService() { + public JWTSigningAndValidationService getSigningAndValidationService() { return signingAndValidationService; } /** * @param signingAndValidationService the signingAndValidationService to set */ - public void setSigningAndValidationService(JwtSigningAndValidationService signingAndValidationService) { + public void setSigningAndValidationService(JWTSigningAndValidationService signingAndValidationService) { this.signingAndValidationService = signingAndValidationService; } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticAuthRequestOptionsService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticAuthRequestOptionsService.java index 87ccb40086..8febc64a09 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticAuthRequestOptionsService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticAuthRequestOptionsService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.client.service.impl; @@ -29,15 +30,16 @@ import org.mitre.openid.connect.config.ServerConfiguration; /** - * + * * Always returns the same set of options. - * + * * @author jricher * */ public class StaticAuthRequestOptionsService implements AuthRequestOptionsService { - private Map options = new HashMap(); + private Map options = new HashMap<>(); + private Map tokenOptions = new HashMap<>(); /* (non-Javadoc) * @see org.mitre.openid.connect.client.service.AuthRequestOptionsService#getOptions(org.mitre.openid.connect.config.ServerConfiguration, org.mitre.oauth2.model.RegisteredClient, javax.servlet.http.HttpServletRequest) @@ -47,8 +49,16 @@ public Map getOptions(ServerConfiguration server, RegisteredClie return options; } + /* (non-Javadoc) + * @see org.mitre.openid.connect.client.service.AuthRequestOptionsService#getTokenOptions(org.mitre.openid.connect.config.ServerConfiguration, org.mitre.oauth2.model.RegisteredClient, javax.servlet.http.HttpServletRequest) + */ + @Override + public Map getTokenOptions(ServerConfiguration server, RegisteredClient client, HttpServletRequest request) { + return tokenOptions; + } + /** - * @return the options + * @return the options object directly */ public Map getOptions() { return options; @@ -61,6 +71,18 @@ public void setOptions(Map options) { this.options = options; } + /** + * @return the tokenOptions + */ + public Map getTokenOptions() { + return tokenOptions; + } + /** + * @param tokenOptions the tokenOptions to set + */ + public void setTokenOptions(Map tokenOptions) { + this.tokenOptions = tokenOptions; + } } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticClientConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticClientConfigurationService.java index 8b1422ad94..df31018047 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticClientConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticClientConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -29,9 +30,9 @@ /** * Client configuration service that holds a static map from issuer URL to a ClientDetails object to use at that issuer. - * + * * Designed to be configured as a bean. - * + * * @author jricher * */ @@ -56,7 +57,7 @@ public void setClients(Map clients) { /** * Get the client configured for this issuer - * + * * @see org.mitre.openid.connect.client.service.ClientConfigurationService#getClientConfiguration(java.lang.String) */ @Override @@ -66,7 +67,7 @@ public RegisteredClient getClientConfiguration(ServerConfiguration issuer) { } @PostConstruct - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { if (clients == null || clients.isEmpty()) { throw new IllegalArgumentException("Clients map cannot be null or empty"); } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticServerConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticServerConfigurationService.java index 3ead65c284..ebca40c1e4 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticServerConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticServerConfigurationService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -28,7 +29,7 @@ /** * Statically configured server configuration service that maps issuer URLs to server configurations to use at that issuer. - * + * * @author jricher * */ @@ -60,7 +61,7 @@ public ServerConfiguration getServerConfiguration(String issuer) { } @PostConstruct - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { if (servers == null || servers.isEmpty()) { throw new IllegalArgumentException("Servers map cannot be null or empty."); } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticSingleIssuerService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticSingleIssuerService.java index 7d9cf8ab92..c72b655236 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticSingleIssuerService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/StaticSingleIssuerService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -51,7 +52,7 @@ public void setIssuer(String issuer) { /** * Always returns the configured issuer URL - * + * * @see org.mitre.openid.connect.client.service.IssuerService#getIssuer(javax.servlet.http.HttpServletRequest) */ @Override @@ -60,7 +61,7 @@ public IssuerServiceResponse getIssuer(HttpServletRequest request) { } @PostConstruct - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { if (Strings.isNullOrEmpty(issuer)) { throw new IllegalArgumentException("Issuer must not be null or empty."); diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/ThirdPartyIssuerService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/ThirdPartyIssuerService.java index e6de8f4191..b26b91c897 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/ThirdPartyIssuerService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/ThirdPartyIssuerService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -34,9 +35,9 @@ import com.google.common.base.Strings; /** - * + * * Determines the issuer using an account chooser or other third-party-initiated login - * + * * @author jricher * */ @@ -44,8 +45,8 @@ public class ThirdPartyIssuerService implements IssuerService { private String accountChooserUrl; - private Set whitelist = new HashSet(); - private Set blacklist = new HashSet(); + private Set whitelist = new HashSet<>(); + private Set blacklist = new HashSet<>(); /* (non-Javadoc) * @see org.mitre.openid.connect.client.service.IssuerService#getIssuer(javax.servlet.http.HttpServletRequest) @@ -127,11 +128,8 @@ public void setBlacklist(Set blacklist) { this.blacklist = blacklist; } - /* (non-Javadoc) - * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() - */ @PostConstruct - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { if (Strings.isNullOrEmpty(this.accountChooserUrl)) { throw new IllegalArgumentException("Account Chooser URL cannot be null or empty"); } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/WebfingerIssuerService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/WebfingerIssuerService.java index ecb71a2c62..ca2fe59494 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/WebfingerIssuerService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/WebfingerIssuerService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; @@ -27,7 +28,7 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.utils.URIBuilder; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.mitre.discovery.util.WebfingerURLNormalizer; import org.mitre.openid.connect.client.model.IssuerServiceResponse; import org.mitre.openid.connect.client.service.IssuerService; @@ -35,6 +36,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; @@ -46,6 +48,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.JsonParser; /** @@ -55,13 +58,26 @@ */ public class WebfingerIssuerService implements IssuerService { - private static Logger logger = LoggerFactory.getLogger(WebfingerIssuerService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(WebfingerIssuerService.class); // map of user input -> issuer, loaded dynamically from webfinger discover - private LoadingCache issuers; + private LoadingCache issuers; + + // private data shuttle class to get back two bits of info from the cache loader + private class LoadingResult { + public String loginHint; + public String issuer; + public LoadingResult(String loginHint, String issuer) { + this.loginHint = loginHint; + this.issuer = issuer; + } + } - private Set whitelist = new HashSet(); - private Set blacklist = new HashSet(); + private Set whitelist = new HashSet<>(); + private Set blacklist = new HashSet<>(); /** * Name of the incoming parameter to check for discovery purposes. @@ -73,8 +89,17 @@ public class WebfingerIssuerService implements IssuerService { */ private String loginPageUrl; + /** + * Strict enfocement of "https" + */ + private boolean forceHttps = true; + public WebfingerIssuerService() { - issuers = CacheBuilder.newBuilder().build(new WebfingerIssuerFetcher()); + this(HttpClientBuilder.create().useSystemProperties().build()); + } + + public WebfingerIssuerService(HttpClient httpClient) { + issuers = CacheBuilder.newBuilder().build(new WebfingerIssuerFetcher(httpClient)); } /* (non-Javadoc) @@ -86,21 +111,18 @@ public IssuerServiceResponse getIssuer(HttpServletRequest request) { String identifier = request.getParameter(parameterName); if (!Strings.isNullOrEmpty(identifier)) { try { - String issuer = issuers.get(WebfingerURLNormalizer.normalizeResource(identifier)); - if (!whitelist.isEmpty() && !whitelist.contains(issuer)) { - throw new AuthenticationServiceException("Whitelist was nonempty, issuer was not in whitelist: " + issuer); + LoadingResult lr = issuers.get(identifier); + if (!whitelist.isEmpty() && !whitelist.contains(lr.issuer)) { + throw new AuthenticationServiceException("Whitelist was nonempty, issuer was not in whitelist: " + lr.issuer); } - if (blacklist.contains(issuer)) { - throw new AuthenticationServiceException("Issuer was in blacklist: " + issuer); + if (blacklist.contains(lr.issuer)) { + throw new AuthenticationServiceException("Issuer was in blacklist: " + lr.issuer); } - return new IssuerServiceResponse(issuer, null, null); - } catch (UncheckedExecutionException ue) { - logger.warn("Issue fetching issuer for user input: " + identifier, ue); - return null; - } catch (ExecutionException e) { - logger.warn("Issue fetching issuer for user input: " + identifier, e); + return new IssuerServiceResponse(lr.issuer, lr.loginHint, request.getParameter("target_link_uri")); + } catch (UncheckedExecutionException | ExecutionException e) { + logger.warn("Issue fetching issuer for user input: " + identifier + ": " + e.getMessage()); return null; } @@ -167,27 +189,52 @@ public void setBlacklist(Set blacklist) { this.blacklist = blacklist; } + /** + * @return the forceHttps + */ + public boolean isForceHttps() { + return forceHttps; + } + + /** + * @param forceHttps the forceHttps to set + */ + public void setForceHttps(boolean forceHttps) { + this.forceHttps = forceHttps; + } + /** * @author jricher * */ - private class WebfingerIssuerFetcher extends CacheLoader { - private HttpClient httpClient = new SystemDefaultHttpClient(); - private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + private class WebfingerIssuerFetcher extends CacheLoader { + private HttpComponentsClientHttpRequestFactory httpFactory; private JsonParser parser = new JsonParser(); + WebfingerIssuerFetcher(HttpClient httpClient) { + this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + } + @Override - public String load(UriComponents key) throws Exception { + public LoadingResult load(String identifier) throws Exception { + + UriComponents key = WebfingerURLNormalizer.normalizeResource(identifier); RestTemplate restTemplate = new RestTemplate(httpFactory); // construct the URL to go to - // preserving http scheme is strictly for demo system use only. String scheme = key.getScheme(); - if (!Strings.isNullOrEmpty(scheme) && scheme.equals("http")) { - scheme = "http://"; // add on colon and slashes. - logger.warn("Webfinger endpoint MUST use the https URI scheme."); + + // preserving http scheme is strictly for demo system use only. + if (!Strings.isNullOrEmpty(scheme) &&scheme.equals("http")) { + if (forceHttps) { + throw new IllegalArgumentException("Scheme must not be 'http'"); + } else { + logger.warn("Webfinger endpoint MUST use the https URI scheme, overriding by configuration"); + scheme = "http://"; // add on colon and slashes. + } } else { + // otherwise we don't know the scheme, assume HTTPS scheme = "https://"; } @@ -199,46 +246,56 @@ public String load(UriComponents key) throws Exception { + "/.well-known/webfinger" + (Strings.isNullOrEmpty(key.getQuery()) ? "" : "?" + key.getQuery()) ); - builder.addParameter("resource", key.toString()); + builder.addParameter("resource", identifier); builder.addParameter("rel", "http://openid.net/specs/connect/1.0/issuer"); - // do the fetch - logger.info("Loading: " + builder.toString()); - String webfingerResponse = restTemplate.getForObject(builder.build(), String.class); - - // TODO: catch and handle HTTP errors - - JsonElement json = parser.parse(webfingerResponse); - - // TODO: catch and handle JSON errors - - if (json != null && json.isJsonObject()) { - // find the issuer - JsonArray links = json.getAsJsonObject().get("links").getAsJsonArray(); - for (JsonElement link : links) { - if (link.isJsonObject()) { - JsonObject linkObj = link.getAsJsonObject(); - if (linkObj.has("href") - && linkObj.has("rel") - && linkObj.get("rel").getAsString().equals("http://openid.net/specs/connect/1.0/issuer")) { + try { - // we found the issuer, return it - return linkObj.get("href").getAsString(); + // do the fetch + logger.info("Loading: " + builder.toString()); + String webfingerResponse = restTemplate.getForObject(builder.build(), String.class); + + JsonElement json = parser.parse(webfingerResponse); + + if (json != null && json.isJsonObject()) { + // find the issuer + JsonArray links = json.getAsJsonObject().get("links").getAsJsonArray(); + for (JsonElement link : links) { + if (link.isJsonObject()) { + JsonObject linkObj = link.getAsJsonObject(); + if (linkObj.has("href") + && linkObj.has("rel") + && linkObj.get("rel").getAsString().equals("http://openid.net/specs/connect/1.0/issuer")) { + + // we found the issuer, return it + String href = linkObj.get("href").getAsString(); + + if (identifier.equals(href) + || identifier.startsWith("http")) { + // try to avoid sending a URL as the login hint + return new LoadingResult(null, href); + } else { + // otherwise pass back whatever the user typed as a login hint + return new LoadingResult(identifier, href); + } + } } } } + } catch (JsonParseException | RestClientException e) { + logger.warn("Failure in fetching webfinger input", e.getMessage()); } - // we couldn't find it + // we couldn't find it! if (key.getScheme().equals("http") || key.getScheme().equals("https")) { - // if it looks like HTTP then punt and return the input - logger.warn("Returning normalized input string as issuer, hoping for the best: " + key.toString()); - return key.toString(); + // if it looks like HTTP then punt: return the input, hope for the best + logger.warn("Returning normalized input string as issuer, hoping for the best: " + identifier); + return new LoadingResult(null, identifier); } else { // if it's not HTTP, give up - logger.warn("Couldn't find issuer: " + key.toString()); - return null; + logger.warn("Couldn't find issuer: " + identifier); + throw new IllegalArgumentException(); } } diff --git a/openid-connect-client/src/test/java/org/mitre/oauth2/introspectingfilter/TestOAuth2AccessTokenImpl.java b/openid-connect-client/src/test/java/org/mitre/oauth2/introspectingfilter/TestOAuth2AccessTokenImpl.java new file mode 100644 index 0000000000..051b5a26c2 --- /dev/null +++ b/openid-connect-client/src/test/java/org/mitre/oauth2/introspectingfilter/TestOAuth2AccessTokenImpl.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.introspectingfilter; + +import java.util.Collections; +import java.util.Date; +import java.util.Set; + +import org.junit.Test; + +import com.google.common.collect.ImmutableSet; +import com.google.gson.JsonObject; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.junit.Assert.assertThat; + +public class TestOAuth2AccessTokenImpl { + + private static String tokenString = "thisisatokenstring"; + + private static Set scopes = ImmutableSet.of("bar", "foo"); + private static String scopeString = "foo bar"; + + private static Date exp = new Date(123 * 1000L); + private static Long expVal = 123L; + + @Test + public void testFullToken() { + + + JsonObject tokenObj = new JsonObject(); + tokenObj.addProperty("active", true); + tokenObj.addProperty("scope", scopeString); + tokenObj.addProperty("exp", expVal); + tokenObj.addProperty("sub", "subject"); + tokenObj.addProperty("client_id", "123-456-789"); + + OAuth2AccessTokenImpl tok = new OAuth2AccessTokenImpl(tokenObj, tokenString); + + assertThat(tok.getScope(), is(equalTo(scopes))); + assertThat(tok.getExpiration(), is(equalTo(exp))); + } + + @Test + public void testNullExp() { + + + JsonObject tokenObj = new JsonObject(); + tokenObj.addProperty("active", true); + tokenObj.addProperty("scope", scopeString); + tokenObj.addProperty("sub", "subject"); + tokenObj.addProperty("client_id", "123-456-789"); + + OAuth2AccessTokenImpl tok = new OAuth2AccessTokenImpl(tokenObj, tokenString); + + assertThat(tok.getScope(), is(equalTo(scopes))); + assertThat(tok.getExpiration(), is(equalTo(null))); + } + + @Test + public void testNullScopes() { + + + JsonObject tokenObj = new JsonObject(); + tokenObj.addProperty("active", true); + tokenObj.addProperty("exp", expVal); + tokenObj.addProperty("sub", "subject"); + tokenObj.addProperty("client_id", "123-456-789"); + + OAuth2AccessTokenImpl tok = new OAuth2AccessTokenImpl(tokenObj, tokenString); + + assertThat(tok.getScope(), is(equalTo(Collections.EMPTY_SET))); + assertThat(tok.getExpiration(), is(equalTo(exp))); + } + + @Test + public void testNullScopesNullExp() { + + + JsonObject tokenObj = new JsonObject(); + tokenObj.addProperty("active", true); + tokenObj.addProperty("sub", "subject"); + tokenObj.addProperty("client_id", "123-456-789"); + + OAuth2AccessTokenImpl tok = new OAuth2AccessTokenImpl(tokenObj, tokenString); + + assertThat(tok.getScope(), is(equalTo(Collections.EMPTY_SET))); + assertThat(tok.getExpiration(), is(equalTo(null))); + } + +} diff --git a/openid-connect-client/src/test/java/org/mitre/oauth2/introspectingfilter/service/impl/TestScopeBasedIntrospectionAuthoritiesGranter.java b/openid-connect-client/src/test/java/org/mitre/oauth2/introspectingfilter/service/impl/TestScopeBasedIntrospectionAuthoritiesGranter.java new file mode 100644 index 0000000000..2323019eff --- /dev/null +++ b/openid-connect-client/src/test/java/org/mitre/oauth2/introspectingfilter/service/impl/TestScopeBasedIntrospectionAuthoritiesGranter.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.introspectingfilter.service.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import com.google.gson.JsonObject; + +import static org.junit.Assert.assertTrue; + +/** + * @author jricher + * + */ +public class TestScopeBasedIntrospectionAuthoritiesGranter { + + private JsonObject introspectionResponse; + + private ScopeBasedIntrospectionAuthoritiesGranter granter = new ScopeBasedIntrospectionAuthoritiesGranter(); + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + introspectionResponse = new JsonObject(); + } + + /** + * Test method for {@link org.mitre.oauth2.introspectingfilter.service.impl.ScopeBasedIntrospectionAuthoritiesGranter#getAuthorities(com.google.gson.JsonObject)}. + */ + @Test + public void testGetAuthoritiesJsonObject_withScopes() { + introspectionResponse.addProperty("scope", "foo bar baz batman"); + + List expected = new ArrayList<>(); + expected.add(new SimpleGrantedAuthority("ROLE_API")); + expected.add(new SimpleGrantedAuthority("OAUTH_SCOPE_foo")); + expected.add(new SimpleGrantedAuthority("OAUTH_SCOPE_bar")); + expected.add(new SimpleGrantedAuthority("OAUTH_SCOPE_baz")); + expected.add(new SimpleGrantedAuthority("OAUTH_SCOPE_batman")); + + List authorities = granter.getAuthorities(introspectionResponse); + + assertTrue(authorities.containsAll(expected)); + assertTrue(expected.containsAll(authorities)); + } + + /** + * Test method for {@link org.mitre.oauth2.introspectingfilter.service.impl.ScopeBasedIntrospectionAuthoritiesGranter#getAuthorities(com.google.gson.JsonObject)}. + */ + @Test + public void testGetAuthoritiesJsonObject_withoutScopes() { + + List expected = new ArrayList<>(); + expected.add(new SimpleGrantedAuthority("ROLE_API")); + + List authorities = granter.getAuthorities(introspectionResponse); + + assertTrue(authorities.containsAll(expected)); + assertTrue(expected.containsAll(authorities)); + } + +} diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/TestOIDCAuthenticationFilter.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/TestOIDCAuthenticationFilter.java new file mode 100644 index 0000000000..ae3018bbc8 --- /dev/null +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/TestOIDCAuthenticationFilter.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.client; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.security.authentication.AuthenticationServiceException; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; + +import static org.mockito.Mockito.mock; + +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +public class TestOIDCAuthenticationFilter { + + private OIDCAuthenticationFilter filter = new OIDCAuthenticationFilter(); + + @Test + public void attemptAuthentication_error() throws Exception { + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.when(request.getParameter("error")).thenReturn("Error"); + Mockito.when(request.getParameter("error_description")).thenReturn("Description"); + Mockito.when(request.getParameter("error_uri")).thenReturn("http://example.com"); + + try { + filter.attemptAuthentication(request, mock(HttpServletResponse.class)); + + fail("AuthorizationEndpointException expected."); + } + catch (AuthorizationEndpointException exception) { + assertThat(exception.getMessage(), + is("Error from Authorization Endpoint: Error Description http://example.com")); + + assertThat(exception.getError(), is("Error")); + assertThat(exception.getErrorDescription(), is("Description")); + assertThat(exception.getErrorURI(), is("http://example.com")); + + assertThat(exception, is(instanceOf(AuthenticationServiceException.class))); + } + } +} diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridClientConfigurationService.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridClientConfigurationService.java index 373fb420b5..f7455981d9 100644 --- a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridClientConfigurationService.java +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridClientConfigurationService.java @@ -1,26 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,6 +28,12 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + /** * @author wkim * diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridServerConfigurationService.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridServerConfigurationService.java index 07827c2672..c14e756f14 100644 --- a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridServerConfigurationService.java +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestHybridServerConfigurationService.java @@ -1,27 +1,23 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,6 +28,12 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + /** * @author wkim * diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestPlainAuthRequestUrlBuilder.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestPlainAuthRequestUrlBuilder.java index 6f7a735080..391afb612c 100644 --- a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestPlainAuthRequestUrlBuilder.java +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestPlainAuthRequestUrlBuilder.java @@ -1,24 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; - import java.util.Map; import org.junit.Before; @@ -31,6 +29,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; +import static org.hamcrest.CoreMatchers.equalTo; + +import static org.junit.Assert.assertThat; + /** * @author wkim * @@ -68,7 +70,27 @@ public void buildAuthRequestUrl() { Map options = ImmutableMap.of("foo", "bar"); - String actualUrl = urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, "https://client.example.org/", "34fasf3ds", "af0ifjsldkj", options); + String actualUrl = urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, "https://client.example.org/", "34fasf3ds", "af0ifjsldkj", options, null); + + assertThat(actualUrl, equalTo(expectedUrl)); + } + + @Test + public void buildAuthRequestUrl_withLoginHint() { + + String expectedUrl = "https://server.example.com/authorize?" + + "response_type=code" + + "&client_id=s6BhdRkqt3" + + "&scope=openid+profile" + // plus sign used for space per application/x-www-form-encoded standard + "&redirect_uri=https%3A%2F%2Fclient.example.org%2F" + + "&nonce=34fasf3ds" + + "&state=af0ifjsldkj" + + "&foo=bar" + + "&login_hint=bob"; + + Map options = ImmutableMap.of("foo", "bar"); + + String actualUrl = urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, "https://client.example.org/", "34fasf3ds", "af0ifjsldkj", options, "bob"); assertThat(actualUrl, equalTo(expectedUrl)); } @@ -80,7 +102,7 @@ public void buildAuthRequestUrl_badUri() { Map options = ImmutableMap.of("foo", "bar"); - urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, "example.com", "", "", options); + urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, "example.com", "", "", options, null); } } diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestSignedAuthRequestUrlBuilder.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestSignedAuthRequestUrlBuilder.java index 34eead9dd3..1e733cadbf 100644 --- a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestSignedAuthRequestUrlBuilder.java +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestSignedAuthRequestUrlBuilder.java @@ -1,25 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.net.URI; import java.net.URISyntaxException; import java.security.NoSuchAlgorithmException; @@ -31,7 +28,7 @@ import org.junit.Before; import org.junit.Test; -import org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.DefaultJWTSigningAndValidationService; import org.mitre.oauth2.model.RegisteredClient; import org.mitre.openid.connect.config.ServerConfiguration; import org.mockito.Mockito; @@ -44,15 +41,19 @@ import com.google.common.collect.Sets; import com.nimbusds.jose.Algorithm; import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.util.Base64URL; -import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; +import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + /** * @author wkim - * + * */ public class TestSignedAuthRequestUrlBuilder { @@ -82,19 +83,20 @@ public class TestSignedAuthRequestUrlBuilder { "9Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q"; private String alg = "RS256"; private String kid = "2011-04-29"; + private String loginHint = "bob"; - private DefaultJwtSigningAndValidationService signingAndValidationService; + private DefaultJWTSigningAndValidationService signingAndValidationService; private SignedAuthRequestUrlBuilder urlBuilder = new SignedAuthRequestUrlBuilder(); @Before public void prepare() throws NoSuchAlgorithmException, InvalidKeySpecException { - RSAKey key = new RSAKey(new Base64URL(n), new Base64URL(e), new Base64URL(d), KeyUse.SIGNATURE, null, new Algorithm(alg), kid, null, null, null); + RSAKey key = new RSAKey(new Base64URL(n), new Base64URL(e), new Base64URL(d), KeyUse.SIGNATURE, null, new Algorithm(alg), kid, null, null, null, null, null); Map keys = Maps.newHashMap(); keys.put("client", key); - signingAndValidationService = new DefaultJwtSigningAndValidationService(keys); + signingAndValidationService = new DefaultJWTSigningAndValidationService(keys); signingAndValidationService.setDefaultSignerKeyId("client"); signingAndValidationService.setDefaultSigningAlgorithmName(alg); @@ -116,7 +118,46 @@ public void prepare() throws NoSuchAlgorithmException, InvalidKeySpecException { @Test public void buildAuthRequestUrl() { - String requestUri = urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, redirectUri, nonce, state, options); + String requestUri = urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, redirectUri, nonce, state, options, null); + + // parsing the result + UriComponentsBuilder builder = null; + + try { + builder = UriComponentsBuilder.fromUri(new URI(requestUri)); + } catch (URISyntaxException e1) { + fail("URISyntaxException was thrown."); + } + + UriComponents components = builder.build(); + String jwtString = components.getQueryParams().get("request").get(0); + JWTClaimsSet claims = null; + + try { + SignedJWT jwt = SignedJWT.parse(jwtString); + claims = jwt.getJWTClaimsSet(); + } catch (ParseException e) { + fail("ParseException was thrown."); + } + + assertEquals(responseType, claims.getClaim("response_type")); + assertEquals(clientConfig.getClientId(), claims.getClaim("client_id")); + + List scopeList = Arrays.asList(((String) claims.getClaim("scope")).split(" ")); + assertTrue(scopeList.containsAll(clientConfig.getScope())); + + assertEquals(redirectUri, claims.getClaim("redirect_uri")); + assertEquals(nonce, claims.getClaim("nonce")); + assertEquals(state, claims.getClaim("state")); + for (String claim : options.keySet()) { + assertEquals(options.get(claim), claims.getClaim(claim)); + } + } + + @Test + public void buildAuthRequestUrl_withLoginHint() { + + String requestUri = urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, redirectUri, nonce, state, options, loginHint); // parsing the result UriComponentsBuilder builder = null; @@ -129,7 +170,7 @@ public void buildAuthRequestUrl() { UriComponents components = builder.build(); String jwtString = components.getQueryParams().get("request").get(0); - ReadOnlyJWTClaimsSet claims = null; + JWTClaimsSet claims = null; try { SignedJWT jwt = SignedJWT.parse(jwtString); @@ -150,6 +191,7 @@ public void buildAuthRequestUrl() { for (String claim : options.keySet()) { assertEquals(options.get(claim), claims.getClaim(claim)); } + assertEquals(loginHint, claims.getClaim("login_hint")); } @Test(expected = AuthenticationServiceException.class) @@ -157,6 +199,6 @@ public void buildAuthRequestUrl_badUri() { Mockito.when(serverConfig.getAuthorizationEndpointUri()).thenReturn("e=mc^2"); - urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, "example.com", "", "", options); + urlBuilder.buildAuthRequestUrl(serverConfig, clientConfig, "example.com", "", "", options, null); } } diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticClientConfigurationService.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticClientConfigurationService.java index 1c921bc5d5..4f251a4e3c 100644 --- a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticClientConfigurationService.java +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticClientConfigurationService.java @@ -1,27 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - import java.util.HashMap; import java.util.Map; @@ -34,6 +29,13 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + /** * @author wkim * @@ -56,7 +58,7 @@ public void prepare() { service = new StaticClientConfigurationService(); - Map clients = new HashMap(); + Map clients = new HashMap<>(); clients.put(issuer, mockClient); service.setClients(clients); diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticServerConfigurationService.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticServerConfigurationService.java index 6e819949a4..9f86bd3469 100644 --- a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticServerConfigurationService.java +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestStaticServerConfigurationService.java @@ -1,27 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - import java.util.HashMap; import java.util.Map; @@ -32,6 +27,13 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + /** * @author wkim * @@ -52,7 +54,7 @@ public void prepare() { service = new StaticServerConfigurationService(); - Map servers = new HashMap(); + Map servers = new HashMap<>(); servers.put(issuer, mockServerConfig); service.setServers(servers); diff --git a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestThirdPartyIssuerService.java b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestThirdPartyIssuerService.java index f32a07fd46..7a54e7d16c 100644 --- a/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestThirdPartyIssuerService.java +++ b/openid-connect-client/src/test/java/org/mitre/openid/connect/client/service/impl/TestThirdPartyIssuerService.java @@ -1,25 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.client.service.impl; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; - import javax.servlet.http.HttpServletRequest; import org.junit.Before; @@ -30,6 +27,11 @@ import com.google.common.collect.Sets; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertThat; + /** * @author wkim * diff --git a/openid-connect-client/src/test/resources/test-context.xml b/openid-connect-client/src/test/resources/test-context.xml index d96dd5d9bf..51d33c9227 100644 --- a/openid-connect-client/src/test/resources/test-context.xml +++ b/openid-connect-client/src/test/resources/test-context.xml @@ -1,20 +1,21 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> - 4.0.0 - - openid-connect-parent - org.mitre - 1.1.10-SNAPSHOT - .. - - openid-connect-common - OpenID Connect Common modules - OpenID Connect Common - - - org.springframework - spring-core - ${org.springframework-version} - - - org.springframework - spring-webmvc - ${org.springframework-version} - - - org.springframework.security - spring-security-core - ${spring.security.version} - - - org.springframework - * - - - - - com.google.guava - guava - 16.0 - - - org.apache.httpcomponents - httpclient - 4.2.3 - - - org.springframework.security.oauth - 2.0.2.RELEASE - spring-security-oauth2 - - - com.nimbusds - nimbus-jose-jwt - 2.26.1 - - - org.eclipse.persistence - javax.persistence - 2.1.0 - - - com.google.code.gson - gson - 2.0 - - - - - org.slf4j - slf4j-api - ${org.slf4j-version} - - + 4.0.0 + + openid-connect-parent + org.mitre + 1.3.5-SNAPSHOT + .. + + openid-connect-common + OpenID Connect Common modules + OpenID Connect Common + + + org.springframework + spring-core + + + commons-logging + commons-logging + + + + + org.springframework + spring-webmvc + + + org.springframework.security + spring-security-core + + + com.google.guava + guava + + + org.apache.httpcomponents + httpclient + + + org.springframework.security.oauth + spring-security-oauth2 + + + com.nimbusds + nimbus-jose-jwt + + + org.eclipse.persistence + javax.persistence + + + com.google.code.gson + gson + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + org.bouncycastle + bcprov-jdk15on + + + javax.annotation + javax.annotation-api + + + jakarta.xml.bind + jakarta.xml.bind-api + + + javax.xml.bind + jaxb-api + + + javax.activation + activation + + + org.glassfish.jaxb + jaxb-runtime + + + + jar - - jar - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java-version} - ${java-version} - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-sources - - jar - - - - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java-version} + ${java-version} + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-sources + + jar + + + + + + diff --git a/openid-connect-common/src/main/java/org/mitre/data/AbstractPageOperationTemplate.java b/openid-connect-common/src/main/java/org/mitre/data/AbstractPageOperationTemplate.java new file mode 100644 index 0000000000..1962366f8c --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/data/AbstractPageOperationTemplate.java @@ -0,0 +1,206 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.data; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract class for performing an operation on a potentially large + * number of items by paging through the items in discreet chunks. + * + * @param the type parameter + * @author Colm Smyth. + */ +public abstract class AbstractPageOperationTemplate { + + private static final Logger logger = LoggerFactory.getLogger(AbstractPageOperationTemplate.class); + + private static int DEFAULT_MAX_PAGES = 1000; + private static long DEFAULT_MAX_TIME_MILLIS = 600000L; //10 Minutes + + /** + * int specifying the maximum number of + * pages which should be fetched before + * execution should terminate + */ + private int maxPages; + + /** + * long specifying the maximum execution time + * in milliseconds + */ + private long maxTime; + + /** + * boolean specifying whether or not Exceptions + * incurred performing the operation should be + * swallowed during execution default true. + */ + private boolean swallowExceptions = true; + + /** + * String that is used for logging in final tallies. + */ + private String operationName = ""; + + + /** + * default constructor which sets the value of + * maxPages and maxTime to DEFAULT_MAX_PAGES and + * DEFAULT_MAX_TIME_MILLIS respectively + */ + public AbstractPageOperationTemplate(String operationName){ + this(DEFAULT_MAX_PAGES, DEFAULT_MAX_TIME_MILLIS, operationName); + } + + /** + * Instantiates a new AbstractPageOperationTemplate with the + * given maxPages and maxTime + * + * @param maxPages the maximum number of pages to fetch. + * @param maxTime the maximum execution time. + */ + public AbstractPageOperationTemplate(int maxPages, long maxTime, String operationName){ + this.maxPages = maxPages; + this.maxTime = maxTime; + this.operationName = operationName; + } + + /** + * Execute the operation on each member of a page of results + * retrieved through the fetch method. the method will execute + * until either the maxPages or maxTime limit is reached or until + * the fetch method returns no more results. Exceptions thrown + * performing the operation on the item will be swallowed if the + * swallowException (default true) field is set true. + */ + public void execute(){ + logger.debug("[" + getOperationName() + "] Starting execution of paged operation. maximum time: " + maxTime + ", maximum pages: " + maxPages); + + long startTime = System.currentTimeMillis(); + long executionTime = 0; + int i = 0; + + int exceptionsSwallowedCount = 0; + int operationsCompleted = 0; + Set exceptionsSwallowedClasses = new HashSet(); + + + while (i< maxPages && executionTime < maxTime){ + Collection page = fetchPage(); + if(page == null || page.size() == 0){ + break; + } + + for (T item : page) { + try { + doOperation(item); + operationsCompleted++; + } catch (Exception e){ + if(swallowExceptions){ + exceptionsSwallowedCount++; + exceptionsSwallowedClasses.add(e.getClass().getName()); + logger.debug("Swallowing exception " + e.getMessage(), e); + } else { + logger.debug("Rethrowing exception " + e.getMessage()); + throw e; + } + } + } + + i++; + executionTime = System.currentTimeMillis() - startTime; + } + + finalReport(operationsCompleted, exceptionsSwallowedCount, exceptionsSwallowedClasses); + } + + + + /** + * method responsible for fetching + * a page of items. + * + * @return the collection of items + */ + public abstract Collection fetchPage(); + + /** + * method responsible for performing desired + * operation on a fetched page item. + * + * @param item the item + */ + protected abstract void doOperation(T item); + + /** + * Method responsible for final report of progress. + * @return + */ + protected void finalReport(int operationsCompleted, int exceptionsSwallowedCount, Set exceptionsSwallowedClasses) { + if (operationsCompleted > 0 || exceptionsSwallowedCount > 0) { + logger.info("[" + getOperationName() + "] Paged operation run: completed " + operationsCompleted + "; swallowed " + exceptionsSwallowedCount + " exceptions"); + } + for(String className: exceptionsSwallowedClasses) { + logger.warn("[" + getOperationName() + "] Paged operation swallowed at least one exception of type " + className); + } + } + + public int getMaxPages() { + return maxPages; + } + + public void setMaxPages(int maxPages) { + this.maxPages = maxPages; + } + + public long getMaxTime() { + return maxTime; + } + + public void setMaxTime(long maxTime) { + this.maxTime = maxTime; + } + + public boolean isSwallowExceptions() { + return swallowExceptions; + } + + public void setSwallowExceptions(boolean swallowExceptions) { + this.swallowExceptions = swallowExceptions; + } + + + /** + * @return the operationName + */ + public String getOperationName() { + return operationName; + } + + + /** + * @param operationName the operationName to set + */ + public void setOperationName(String operationName) { + this.operationName = operationName; + } +} diff --git a/openid-connect-common/src/main/java/org/mitre/data/DefaultPageCriteria.java b/openid-connect-common/src/main/java/org/mitre/data/DefaultPageCriteria.java new file mode 100644 index 0000000000..daec512b0a --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/data/DefaultPageCriteria.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.data; + +/** + * Default implementation of PageCriteria which specifies + * both page to be retrieved and page size in the constructor. + * + * @author Colm Smyth + */ +public class DefaultPageCriteria implements PageCriteria { + + private static final int DEFAULT_PAGE_NUMBER = 0; + private static final int DEFAULT_PAGE_SIZE = 100; + + private int pageNumber; + private int pageSize; + + public DefaultPageCriteria(){ + this(DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE); + } + + public DefaultPageCriteria(int pageNumber, int pageSize) { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + } + + @Override + public int getPageNumber() { + return pageNumber; + } + + @Override + public int getPageSize() { + return pageSize; + } +} diff --git a/openid-connect-common/src/main/java/org/mitre/data/PageCriteria.java b/openid-connect-common/src/main/java/org/mitre/data/PageCriteria.java new file mode 100644 index 0000000000..2db1077d94 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/data/PageCriteria.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.data; + +/** + * Interface which defines page criteria for use in + * a repository operation. + * + * @author Colm Smyth + */ +public interface PageCriteria { + + public int getPageNumber(); + public int getPageSize(); +} diff --git a/openid-connect-common/src/main/java/org/mitre/discovery/util/WebfingerURLNormalizer.java b/openid-connect-common/src/main/java/org/mitre/discovery/util/WebfingerURLNormalizer.java index 84e63cf400..b71fbf6d6a 100644 --- a/openid-connect-common/src/main/java/org/mitre/discovery/util/WebfingerURLNormalizer.java +++ b/openid-connect-common/src/main/java/org/mitre/discovery/util/WebfingerURLNormalizer.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.discovery.util; import java.util.regex.Matcher; @@ -29,13 +30,16 @@ /** * Provides utility methods for normalizing and parsing URIs for use with Webfinger Discovery. - * + * * @author wkim * */ public class WebfingerURLNormalizer { - private static Logger logger = LoggerFactory.getLogger(WebfingerURLNormalizer.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(WebfingerURLNormalizer.class); // pattern used to parse user input; we can't use the built-in java URI parser private static final Pattern pattern = Pattern.compile("^" + diff --git a/openid-connect-common/src/main/java/org/mitre/jose/JWEAlgorithmEmbed.java b/openid-connect-common/src/main/java/org/mitre/jose/JWEAlgorithmEmbed.java deleted file mode 100644 index 8e6fd44b37..0000000000 --- a/openid-connect-common/src/main/java/org/mitre/jose/JWEAlgorithmEmbed.java +++ /dev/null @@ -1,110 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.jose; - -import javax.persistence.Basic; -import javax.persistence.Embeddable; -import javax.persistence.Transient; - -import com.google.common.base.Strings; -import com.nimbusds.jose.JWEAlgorithm; - -/** - * - * Wrapper class for Nimbus JOSE objects to fit into JPA - * - * @author jricher - * - */ -@Embeddable -public class JWEAlgorithmEmbed { - - public static final JWEAlgorithmEmbed NONE = getForAlgorithmName("none"); - - private JWEAlgorithm algorithm; - - public JWEAlgorithmEmbed() { - - } - - public JWEAlgorithmEmbed(JWEAlgorithm algorithm) { - this.algorithm = algorithm; - } - - public static JWEAlgorithmEmbed getForAlgorithmName (String algorithmName) { - JWEAlgorithmEmbed ent = new JWEAlgorithmEmbed(); - ent.setAlgorithmName(algorithmName); - if (ent.getAlgorithm() == null) { - return null; - } else { - return ent; - } - } - - /** - * Get the name of this algorithm, return null if no algorithm set. - * @return - */ - @Basic - public String getAlgorithmName() { - if (algorithm != null) { - return algorithm.getName(); - } else { - return null; - } - } - - /** - * Set the name of this algorithm. - * Calls JWEAlgorithm.parse() - * @param algorithmName - */ - public void setAlgorithmName(String algorithmName) { - if (!Strings.isNullOrEmpty(algorithmName)) { - algorithm = JWEAlgorithm.parse(algorithmName); - } else { - algorithm = null; - } - } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return "JWEAlgorithmEmbed [algorithm=" + algorithm + "]"; - } - - /** - * @return the algorithm - */ - @Transient - public JWEAlgorithm getAlgorithm() { - return algorithm; - } - - /** - * @param algorithm the algorithm to set - */ - public void setAlgorithm(JWEAlgorithm algorithm) { - this.algorithm = algorithm; - } - -} diff --git a/openid-connect-common/src/main/java/org/mitre/jose/JWEEncryptionMethodEmbed.java b/openid-connect-common/src/main/java/org/mitre/jose/JWEEncryptionMethodEmbed.java deleted file mode 100644 index f70f7e1520..0000000000 --- a/openid-connect-common/src/main/java/org/mitre/jose/JWEEncryptionMethodEmbed.java +++ /dev/null @@ -1,108 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.jose; - -import javax.persistence.Basic; -import javax.persistence.Embeddable; -import javax.persistence.Transient; - -import com.google.common.base.Strings; -import com.nimbusds.jose.EncryptionMethod; - -/** - * @author jricher - * - */ -@Embeddable -public class JWEEncryptionMethodEmbed { - - public static final JWEEncryptionMethodEmbed NONE = getForAlgorithmName("none"); - - private EncryptionMethod algorithm; - - public JWEEncryptionMethodEmbed() { - - } - - public JWEEncryptionMethodEmbed(EncryptionMethod algorithm) { - this.algorithm = algorithm; - } - - public static JWEEncryptionMethodEmbed getForAlgorithmName (String algorithmName) { - JWEEncryptionMethodEmbed ent = new JWEEncryptionMethodEmbed(); - ent.setAlgorithmName(algorithmName); - if (ent.getAlgorithm() == null) { - return null; - } else { - return ent; - } - } - - /** - * Get the name of this algorithm, return null if no algorithm set. - * @return - */ - @Basic - public String getAlgorithmName() { - if (algorithm != null) { - return algorithm.getName(); - } else { - return null; - } - } - - /** - * Set the name of this algorithm. - * Calls EncryptionMethod.parse() - * @param algorithmName - */ - public void setAlgorithmName(String algorithmName) { - if (!Strings.isNullOrEmpty(algorithmName)) { - algorithm = EncryptionMethod.parse(algorithmName); - } else { - algorithm = null; - } - } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return "JWEEncryptionMethodEmbed [algorithm=" + algorithm + "]"; - } - - /** - * @return the algorithm - */ - @Transient - public EncryptionMethod getAlgorithm() { - return algorithm; - } - - /** - * @param algorithm the algorithm to set - */ - public void setAlgorithm(EncryptionMethod algorithm) { - this.algorithm = algorithm; - } - - -} diff --git a/openid-connect-common/src/main/java/org/mitre/jose/JWSAlgorithmEmbed.java b/openid-connect-common/src/main/java/org/mitre/jose/JWSAlgorithmEmbed.java deleted file mode 100644 index d062b063e7..0000000000 --- a/openid-connect-common/src/main/java/org/mitre/jose/JWSAlgorithmEmbed.java +++ /dev/null @@ -1,117 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.jose; - -import javax.persistence.Basic; -import javax.persistence.Embeddable; -import javax.persistence.Transient; - -import com.google.common.base.Strings; -import com.nimbusds.jose.JWSAlgorithm; - -/** - * - * Wrapper class for Nimbus JOSE objects to fit into JPA - * - * @author jricher - * - */ -@Embeddable -public class JWSAlgorithmEmbed { - - public static final JWSAlgorithmEmbed NONE = getForAlgorithmName("none"); - - private JWSAlgorithm algorithm; - - public JWSAlgorithmEmbed() { - - } - - public JWSAlgorithmEmbed(JWSAlgorithm algorithm) { - this.algorithm = algorithm; - } - - /** - * - * @param algorithmName - * @return null if algorithmName is empty or null - */ - public static JWSAlgorithmEmbed getForAlgorithmName (String algorithmName) { - JWSAlgorithmEmbed ent = new JWSAlgorithmEmbed(); - ent.setAlgorithmName(algorithmName); - if (ent.getAlgorithm() == null) { - return null; - } else { - return ent; - } - } - - /** - * Get the name of this algorithm, return null if no algorithm set. - * @return - */ - @Basic - public String getAlgorithmName() { - if (algorithm != null) { - return algorithm.getName(); - } else { - return null; - } - } - - /** - * Set the name of this algorithm. - * Calls JWSAlgorithm.parse() - * @param algorithmName - */ - public void setAlgorithmName(String algorithmName) { - if (!Strings.isNullOrEmpty(algorithmName)) { - algorithm = JWSAlgorithm.parse(algorithmName); - } else { - algorithm = null; - } - } - - /** - * @return the algorithm - */ - @Transient - public JWSAlgorithm getAlgorithm() { - return algorithm; - } - - /** - * @param algorithm the algorithm to set - */ - public void setAlgorithm(JWSAlgorithm algorithm) { - this.algorithm = algorithm; - } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return "JWSAlgorithmEmbed [algorithm=" + algorithm + "]"; - } - - - -} diff --git a/openid-connect-common/src/main/java/org/mitre/jose/keystore/JWKSetKeyStore.java b/openid-connect-common/src/main/java/org/mitre/jose/keystore/JWKSetKeyStore.java index 06c36c3c76..f40997b8dc 100644 --- a/openid-connect-common/src/main/java/org/mitre/jose/keystore/JWKSetKeyStore.java +++ b/openid-connect-common/src/main/java/org/mitre/jose/keystore/JWKSetKeyStore.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.jose.keystore; diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/AssertionValidator.java b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/AssertionValidator.java new file mode 100644 index 0000000000..ffc29a2e94 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/AssertionValidator.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.jwt.assertion; + +import com.nimbusds.jwt.JWT; + +/** + * @author jricher + * + */ +public interface AssertionValidator { + + public boolean isValid(JWT assertion); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/NullAssertionValidator.java b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/NullAssertionValidator.java new file mode 100644 index 0000000000..5fa53e6c85 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/NullAssertionValidator.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.jwt.assertion.impl; + +import org.mitre.jwt.assertion.AssertionValidator; + +import com.nimbusds.jwt.JWT; + +/** + * Reject all assertions passed in. + * + * @author jricher + * + */ +public class NullAssertionValidator implements AssertionValidator { + + /** + * Reject all assertions passed in, always returns false. + */ + @Override + public boolean isValid(JWT assertion) { + return false; + + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/SelfAssertionValidator.java b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/SelfAssertionValidator.java new file mode 100644 index 0000000000..5655c93c73 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/SelfAssertionValidator.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.jwt.assertion.impl; + +import java.text.ParseException; + +import org.mitre.jwt.assertion.AssertionValidator; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.common.base.Strings; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +/** + * Validates all assertions generated by this server + * + * @author jricher + * + */ +@Component("selfAssertionValidator") +public class SelfAssertionValidator implements AssertionValidator { + + private static Logger logger = LoggerFactory.getLogger(SelfAssertionValidator.class); + + @Autowired + private ConfigurationPropertiesBean config; + + @Autowired + private JWTSigningAndValidationService jwtService; + + @Override + public boolean isValid(JWT assertion) { + if (!(assertion instanceof SignedJWT)) { + // unsigned assertion + return false; + } + + JWTClaimsSet claims; + try { + claims = assertion.getJWTClaimsSet(); + } catch (ParseException e) { + logger.debug("Invalid assertion claims"); + return false; + } + + // make sure the issuer exists + if (Strings.isNullOrEmpty(claims.getIssuer())) { + logger.debug("No issuer for assertion, rejecting"); + return false; + } + + // make sure the issuer is us + if (!claims.getIssuer().equals(config.getIssuer())) { + logger.debug("Issuer is not the same as this server, rejecting"); + return false; + } + + // validate the signature based on our public key + if (jwtService.validateSignature((SignedJWT) assertion)) { + return true; + } else { + return false; + } + + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/WhitelistedIssuerAssertionValidator.java b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/WhitelistedIssuerAssertionValidator.java new file mode 100644 index 0000000000..87f5986cea --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/WhitelistedIssuerAssertionValidator.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.jwt.assertion.impl; + +import java.text.ParseException; +import java.util.HashMap; +import java.util.Map; + +import org.mitre.jwt.assertion.AssertionValidator; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.JWKSetCacheService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import com.google.common.base.Strings; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +/** + * Checks to see if the assertion was signed by a particular authority available from a whitelist + * @author jricher + * + */ +public class WhitelistedIssuerAssertionValidator implements AssertionValidator { + + private static Logger logger = LoggerFactory.getLogger(WhitelistedIssuerAssertionValidator.class); + + /** + * Map of issuer -> JWKSetUri + */ + private Map whitelist = new HashMap<>(); + + /** + * @return the whitelist + */ + public Map getWhitelist() { + return whitelist; + } + + /** + * @param whitelist the whitelist to set + */ + public void setWhitelist(Map whitelist) { + this.whitelist = whitelist; + } + + @Autowired + private JWKSetCacheService jwkCache; + + @Override + public boolean isValid(JWT assertion) { + + if (!(assertion instanceof SignedJWT)) { + // unsigned assertion + return false; + } + + JWTClaimsSet claims; + try { + claims = assertion.getJWTClaimsSet(); + } catch (ParseException e) { + logger.debug("Invalid assertion claims"); + return false; + } + + if (Strings.isNullOrEmpty(claims.getIssuer())) { + logger.debug("No issuer for assertion, rejecting"); + return false; + } + + if (!whitelist.containsKey(claims.getIssuer())) { + logger.debug("Issuer is not in whitelist, rejecting"); + return false; + } + + String jwksUri = whitelist.get(claims.getIssuer()); + + JWTSigningAndValidationService validator = jwkCache.getValidator(jwksUri); + + if (validator.validateSignature((SignedJWT) assertion)) { + return true; + } else { + return false; + } + + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/JwtEncryptionAndDecryptionService.java b/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/JWTEncryptionAndDecryptionService.java similarity index 91% rename from openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/JwtEncryptionAndDecryptionService.java rename to openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/JWTEncryptionAndDecryptionService.java index 447c58bc5a..0489af58d9 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/JwtEncryptionAndDecryptionService.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/JWTEncryptionAndDecryptionService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.jwt.encryption.service; import java.util.Collection; @@ -28,7 +29,7 @@ * @author wkim * */ -public interface JwtEncryptionAndDecryptionService { +public interface JWTEncryptionAndDecryptionService { /** * Encrypts the JWT in place with the default encrypter. diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/impl/DefaultJwtEncryptionAndDecryptionService.java b/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/impl/DefaultJWTEncryptionAndDecryptionService.java similarity index 70% rename from openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/impl/DefaultJwtEncryptionAndDecryptionService.java rename to openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/impl/DefaultJWTEncryptionAndDecryptionService.java index bc9e53822d..dbe8a530bb 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/impl/DefaultJwtEncryptionAndDecryptionService.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/encryption/service/impl/DefaultJWTEncryptionAndDecryptionService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.jwt.encryption.service.impl; import java.security.NoSuchAlgorithmException; @@ -27,7 +28,7 @@ import javax.annotation.PostConstruct; import org.mitre.jose.keystore.JWKSetKeyStore; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,8 +41,12 @@ import com.nimbusds.jose.JWEObject; import com.nimbusds.jose.crypto.DirectDecrypter; import com.nimbusds.jose.crypto.DirectEncrypter; +import com.nimbusds.jose.crypto.ECDHDecrypter; +import com.nimbusds.jose.crypto.ECDHEncrypter; import com.nimbusds.jose.crypto.RSADecrypter; import com.nimbusds.jose.crypto.RSAEncrypter; +import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton; +import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.jwk.RSAKey; @@ -50,15 +55,18 @@ * @author wkim * */ -public class DefaultJwtEncryptionAndDecryptionService implements JwtEncryptionAndDecryptionService { +public class DefaultJWTEncryptionAndDecryptionService implements JWTEncryptionAndDecryptionService { - private static Logger logger = LoggerFactory.getLogger(DefaultJwtEncryptionAndDecryptionService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultJWTEncryptionAndDecryptionService.class); // map of identifier to encrypter - private Map encrypters = new HashMap(); + private Map encrypters = new HashMap<>(); // map of identifier to decrypter - private Map decrypters = new HashMap(); + private Map decrypters = new HashMap<>(); private String defaultEncryptionKeyId; @@ -67,18 +75,18 @@ public class DefaultJwtEncryptionAndDecryptionService implements JwtEncryptionAn private JWEAlgorithm defaultAlgorithm; // map of identifier to key - private Map keys = new HashMap(); + private Map keys = new HashMap<>(); /** * Build this service based on the keys given. All public keys will be used to make encrypters, * all private keys will be used to make decrypters. - * + * * @param keys * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException * @throws JOSEException */ - public DefaultJwtEncryptionAndDecryptionService(Map keys) throws NoSuchAlgorithmException, InvalidKeySpecException, JOSEException { + public DefaultJWTEncryptionAndDecryptionService(Map keys) throws NoSuchAlgorithmException, InvalidKeySpecException, JOSEException { this.keys = keys; buildEncryptersAndDecrypters(); } @@ -86,13 +94,13 @@ public DefaultJwtEncryptionAndDecryptionService(Map keys) throws No /** * Build this service based on the given keystore. All keys must have a key * id ({@code kid}) field in order to be used. - * + * * @param keyStore * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException * @throws JOSEException */ - public DefaultJwtEncryptionAndDecryptionService(JWKSetKeyStore keyStore) throws NoSuchAlgorithmException, InvalidKeySpecException, JOSEException { + public DefaultJWTEncryptionAndDecryptionService(JWKSetKeyStore keyStore) throws NoSuchAlgorithmException, InvalidKeySpecException, JOSEException { // convert all keys in the keystore to a map based on key id for (JWK key : keyStore.getKeys()) { @@ -109,12 +117,20 @@ public DefaultJwtEncryptionAndDecryptionService(JWKSetKeyStore keyStore) throws @PostConstruct - public void afterPropertiesSet() throws NoSuchAlgorithmException, InvalidKeySpecException, JOSEException{ + public void afterPropertiesSet() { if (keys == null) { throw new IllegalArgumentException("Encryption and decryption service must have at least one key configured."); } - buildEncryptersAndDecrypters(); + try { + buildEncryptersAndDecrypters(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Encryption and decryption service could not find given algorithm."); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException("Encryption and decryption service saw an invalid key specification."); + } catch (JOSEException e) { + throw new IllegalArgumentException("Encryption and decryption service was unable to process JOSE object."); + } } public String getDefaultEncryptionKeyId() { @@ -212,23 +228,40 @@ private void buildEncryptersAndDecrypters() throws NoSuchAlgorithmException, Inv if (jwk instanceof RSAKey) { // build RSA encrypters and decrypters - RSAEncrypter encrypter = new RSAEncrypter(((RSAKey) jwk).toRSAPublicKey()); // there should always at least be the public key + RSAEncrypter encrypter = new RSAEncrypter((RSAKey) jwk); // there should always at least be the public key + encrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); encrypters.put(id, encrypter); if (jwk.isPrivate()) { // we can decrypt! - RSADecrypter decrypter = new RSADecrypter(((RSAKey) jwk).toRSAPrivateKey()); + RSADecrypter decrypter = new RSADecrypter((RSAKey) jwk); + decrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); decrypters.put(id, decrypter); } else { logger.warn("No private key for key #" + jwk.getKeyID()); } + } else if (jwk instanceof ECKey) { + + // build EC Encrypters and decrypters + + ECDHEncrypter encrypter = new ECDHEncrypter((ECKey) jwk); + encrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); + encrypters.put(id, encrypter); - // TODO: add support for EC keys + if (jwk.isPrivate()) { // we can decrypt too + ECDHDecrypter decrypter = new ECDHDecrypter((ECKey) jwk); + decrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); + decrypters.put(id, decrypter); + } else { + logger.warn("No private key for key # " + jwk.getKeyID()); + } } else if (jwk instanceof OctetSequenceKey) { // build symmetric encrypters and decrypters - DirectEncrypter encrypter = new DirectEncrypter(((OctetSequenceKey) jwk).toByteArray()); - DirectDecrypter decrypter = new DirectDecrypter(((OctetSequenceKey) jwk).toByteArray()); + DirectEncrypter encrypter = new DirectEncrypter((OctetSequenceKey) jwk); + encrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); + DirectDecrypter decrypter = new DirectDecrypter((OctetSequenceKey) jwk); + decrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); encrypters.put(id, encrypter); decrypters.put(id, decrypter); @@ -242,7 +275,7 @@ private void buildEncryptersAndDecrypters() throws NoSuchAlgorithmException, Inv @Override public Map getAllPublicKeys() { - Map pubKeys = new HashMap(); + Map pubKeys = new HashMap<>(); // pull out all public keys for (String keyId : keys.keySet()) { @@ -258,14 +291,14 @@ public Map getAllPublicKeys() { @Override public Collection getAllEncryptionAlgsSupported() { - Set algs = new HashSet(); + Set algs = new HashSet<>(); for (JWEEncrypter encrypter : encrypters.values()) { - algs.addAll(encrypter.supportedAlgorithms()); + algs.addAll(encrypter.supportedJWEAlgorithms()); } for (JWEDecrypter decrypter : decrypters.values()) { - algs.addAll(decrypter.supportedAlgorithms()); + algs.addAll(decrypter.supportedJWEAlgorithms()); } return algs; @@ -276,7 +309,7 @@ public Collection getAllEncryptionAlgsSupported() { */ @Override public Collection getAllEncryptionEncsSupported() { - Set encs = new HashSet(); + Set encs = new HashSet<>(); for (JWEEncrypter encrypter : encrypters.values()) { encs.addAll(encrypter.supportedEncryptionMethods()); diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/JwtSigningAndValidationService.java b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/JWTSigningAndValidationService.java similarity index 91% rename from openid-connect-common/src/main/java/org/mitre/jwt/signer/service/JwtSigningAndValidationService.java rename to openid-connect-common/src/main/java/org/mitre/jwt/signer/service/JWTSigningAndValidationService.java index 61d807b1cd..1bb05f1421 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/JwtSigningAndValidationService.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/JWTSigningAndValidationService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.jwt.signer.service; import java.security.NoSuchAlgorithmException; @@ -24,7 +25,7 @@ import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jwt.SignedJWT; -public interface JwtSigningAndValidationService { +public interface JWTSigningAndValidationService { /** * Get all public keys for this service, mapped by their Key ID @@ -34,7 +35,7 @@ public interface JwtSigningAndValidationService { /** * Checks the signature of the given JWT against all configured signers, * returns true if at least one of the signers validates it. - * + * * @param jwtString * the string representation of the JWT as sent on the wire * @return true if the signature is valid, false if not @@ -45,7 +46,7 @@ public interface JwtSigningAndValidationService { /** * Called to sign a jwt in place for a client that hasn't registered a preferred signing algorithm. * Use the default algorithm to sign. - * + * * @param jwt the jwt to sign * @return the signed jwt * @throws NoSuchAlgorithmException @@ -67,13 +68,15 @@ public interface JwtSigningAndValidationService { /** * Sign a jwt using the selected algorithm. The algorithm is selected using the String parameter values specified * in the JWT spec, section 6. I.E., "HS256" means HMAC with SHA-256 and corresponds to our HmacSigner class. - * + * * @param jwt the jwt to sign * @param alg the name of the algorithm to use, as specified in JWS s.6 * @return the signed jwt */ public void signJwt(SignedJWT jwt, JWSAlgorithm alg); + public String getDefaultSignerKeyId(); + /** * TODO: method to sign a jwt using a specified algorithm and a key id */ diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/ClientKeyCacheService.java b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/ClientKeyCacheService.java new file mode 100644 index 0000000000..3e46b9b4bd --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/ClientKeyCacheService.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.jwt.signer.service.impl; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.mitre.jose.keystore.JWKSetKeyStore; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; +import org.mitre.jwt.encryption.service.impl.DefaultJWTEncryptionAndDecryptionService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.google.common.base.Strings; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; + +/** + * + * Takes in a client and returns the appropriate validator or encrypter for + * that client's registered key types. + * + * @author jricher + * + */ +@Service +public class ClientKeyCacheService { + + private static Logger logger = LoggerFactory.getLogger(ClientKeyCacheService.class); + + @Autowired + private JWKSetCacheService jwksUriCache = new JWKSetCacheService(); + + @Autowired + private SymmetricKeyJWTValidatorCacheService symmetricCache = new SymmetricKeyJWTValidatorCacheService(); + + // cache of validators for by-value JWKs + private LoadingCache jwksValidators; + + // cache of encryptors for by-value JWKs + private LoadingCache jwksEncrypters; + + public ClientKeyCacheService() { + this.jwksValidators = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) // expires 1 hour after fetch + .maximumSize(100) + .build(new JWKSetVerifierBuilder()); + this.jwksEncrypters = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) // expires 1 hour after fetch + .maximumSize(100) + .build(new JWKSetEncryptorBuilder()); + } + + + public JWTSigningAndValidationService getValidator(ClientDetailsEntity client, JWSAlgorithm alg) { + + try { + if (alg.equals(JWSAlgorithm.RS256) + || alg.equals(JWSAlgorithm.RS384) + || alg.equals(JWSAlgorithm.RS512) + || alg.equals(JWSAlgorithm.ES256) + || alg.equals(JWSAlgorithm.ES384) + || alg.equals(JWSAlgorithm.ES512) + || alg.equals(JWSAlgorithm.PS256) + || alg.equals(JWSAlgorithm.PS384) + || alg.equals(JWSAlgorithm.PS512)) { + + // asymmetric key + if (client.getJwks() != null) { + return jwksValidators.get(client.getJwks()); + } else if (!Strings.isNullOrEmpty(client.getJwksUri())) { + return jwksUriCache.getValidator(client.getJwksUri()); + } else { + return null; + } + + } else if (alg.equals(JWSAlgorithm.HS256) + || alg.equals(JWSAlgorithm.HS384) + || alg.equals(JWSAlgorithm.HS512)) { + + // symmetric key + + return symmetricCache.getSymmetricValidtor(client); + + } else { + + return null; + } + } catch (UncheckedExecutionException | ExecutionException e) { + logger.error("Problem loading client validator", e); + return null; + } + + } + + public JWTEncryptionAndDecryptionService getEncrypter(ClientDetailsEntity client) { + + try { + if (client.getJwks() != null) { + return jwksEncrypters.get(client.getJwks()); + } else if (!Strings.isNullOrEmpty(client.getJwksUri())) { + return jwksUriCache.getEncrypter(client.getJwksUri()); + } else { + return null; + } + } catch (UncheckedExecutionException | ExecutionException e) { + logger.error("Problem loading client encrypter", e); + return null; + } + + } + + + private class JWKSetEncryptorBuilder extends CacheLoader { + + @Override + public JWTEncryptionAndDecryptionService load(JWKSet key) throws Exception { + return new DefaultJWTEncryptionAndDecryptionService(new JWKSetKeyStore(key)); + } + + } + + private class JWKSetVerifierBuilder extends CacheLoader { + + @Override + public JWTSigningAndValidationService load(JWKSet key) throws Exception { + return new DefaultJWTSigningAndValidationService(new JWKSetKeyStore(key)); + } + + } + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJwtSigningAndValidationService.java b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJWTSigningAndValidationService.java similarity index 72% rename from openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJwtSigningAndValidationService.java rename to openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJWTSigningAndValidationService.java index 32a27bfcbc..eea51fcfd2 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJwtSigningAndValidationService.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJWTSigningAndValidationService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.jwt.signer.service.impl; import java.security.NoSuchAlgorithmException; @@ -26,7 +27,7 @@ import java.util.UUID; import org.mitre.jose.keystore.JWKSetKeyStore; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +36,8 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.ECDSAVerifier; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jose.crypto.RSASSASigner; @@ -45,36 +48,39 @@ import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.SignedJWT; -public class DefaultJwtSigningAndValidationService implements JwtSigningAndValidationService { +public class DefaultJWTSigningAndValidationService implements JWTSigningAndValidationService { // map of identifier to signer - private Map signers = new HashMap(); + private Map signers = new HashMap<>(); // map of identifier to verifier - private Map verifiers = new HashMap(); + private Map verifiers = new HashMap<>(); - private static Logger logger = LoggerFactory.getLogger(DefaultJwtSigningAndValidationService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultJWTSigningAndValidationService.class); private String defaultSignerKeyId; private JWSAlgorithm defaultAlgorithm; // map of identifier to key - private Map keys = new HashMap(); + private Map keys = new HashMap<>(); /** * Build this service based on the keys given. All public keys will be used * to make verifiers, all private keys will be used to make signers. - * + * * @param keys * A map of key identifier to key - * + * * @throws InvalidKeySpecException * If the keys in the JWKs are not valid * @throws NoSuchAlgorithmException * If there is no appropriate algorithm to tie the keys to. */ - public DefaultJwtSigningAndValidationService(Map keys) throws NoSuchAlgorithmException, InvalidKeySpecException { + public DefaultJWTSigningAndValidationService(Map keys) throws NoSuchAlgorithmException, InvalidKeySpecException { this.keys = keys; buildSignersAndVerifiers(); } @@ -82,22 +88,21 @@ public DefaultJwtSigningAndValidationService(Map keys) throws NoSuc /** * Build this service based on the given keystore. All keys must have a key * id ({@code kid}) field in order to be used. - * + * * @param keyStore * the keystore to load all keys from - * + * * @throws InvalidKeySpecException * If the keys in the JWKs are not valid * @throws NoSuchAlgorithmException * If there is no appropriate algorithm to tie the keys to. */ - public DefaultJwtSigningAndValidationService(JWKSetKeyStore keyStore) throws NoSuchAlgorithmException, InvalidKeySpecException { + public DefaultJWTSigningAndValidationService(JWKSetKeyStore keyStore) throws NoSuchAlgorithmException, InvalidKeySpecException { // convert all keys in the keystore to a map based on key id if (keyStore!= null && keyStore.getJwkSet() != null) { for (JWK key : keyStore.getKeys()) { if (!Strings.isNullOrEmpty(key.getKeyID())) { // use the key ID that's built into the key itself - // TODO (#641): deal with JWK thumbprints this.keys.put(key.getKeyID(), key); } else { // create a random key id @@ -113,8 +118,9 @@ public DefaultJwtSigningAndValidationService(JWKSetKeyStore keyStore) throws NoS /** * @return the defaultSignerKeyId */ + @Override public String getDefaultSignerKeyId() { - return defaultSignerKeyId; + return defaultSignerKeyId; } /** @@ -155,39 +161,48 @@ private void buildSignersAndVerifiers() throws NoSuchAlgorithmException, Invalid String id = jwkEntry.getKey(); JWK jwk = jwkEntry.getValue(); - if (jwk instanceof RSAKey) { - // build RSA signers & verifiers + try { + if (jwk instanceof RSAKey) { + // build RSA signers & verifiers - if (jwk.isPrivate()) { // only add the signer if there's a private key - RSASSASigner signer = new RSASSASigner(((RSAKey) jwk).toRSAPrivateKey()); - signers.put(id, signer); - } + if (jwk.isPrivate()) { // only add the signer if there's a private key + RSASSASigner signer = new RSASSASigner((RSAKey) jwk); + signers.put(id, signer); + } - RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) jwk).toRSAPublicKey()); - verifiers.put(id, verifier); + RSASSAVerifier verifier = new RSASSAVerifier((RSAKey) jwk); + verifiers.put(id, verifier); - } else if (jwk instanceof ECKey) { - // build EC signers & verifiers + } else if (jwk instanceof ECKey) { + // build EC signers & verifiers - // TODO: add support for EC keys - logger.warn("EC Keys are not yet supported."); + if (jwk.isPrivate()) { + ECDSASigner signer = new ECDSASigner((ECKey) jwk); + signers.put(id, signer); + } - } else if (jwk instanceof OctetSequenceKey) { - // build HMAC signers & verifiers + ECDSAVerifier verifier = new ECDSAVerifier((ECKey) jwk); + verifiers.put(id, verifier); - if (jwk.isPrivate()) { // technically redundant check because all HMAC keys are private - MACSigner signer = new MACSigner(((OctetSequenceKey) jwk).toByteArray()); - signers.put(id, signer); - } + } else if (jwk instanceof OctetSequenceKey) { + // build HMAC signers & verifiers - MACVerifier verifier = new MACVerifier(((OctetSequenceKey) jwk).toByteArray()); - verifiers.put(id, verifier); + if (jwk.isPrivate()) { // technically redundant check because all HMAC keys are private + MACSigner signer = new MACSigner((OctetSequenceKey) jwk); + signers.put(id, signer); + } - } else { - logger.warn("Unknown key type: " + jwk); + MACVerifier verifier = new MACVerifier((OctetSequenceKey) jwk); + verifiers.put(id, verifier); + + } else { + logger.warn("Unknown key type: " + jwk); + } + } catch (JOSEException e) { + logger.warn("Exception loading signer/verifier", e); } } - + if (defaultSignerKeyId == null && keys.size() == 1) { // if there's only one key, it's the default setDefaultSignerKeyId(keys.keySet().iterator().next()); @@ -220,7 +235,7 @@ public void signJwt(SignedJWT jwt, JWSAlgorithm alg) { JWSSigner signer = null; for (JWSSigner s : signers.values()) { - if (s.supportedAlgorithms().contains(alg)) { + if (s.supportedJWSAlgorithms().contains(alg)) { signer = s; break; } @@ -251,7 +266,7 @@ public boolean validateSignature(SignedJWT jwt) { } } catch (JOSEException e) { - logger.error("Failed to validate signature, error was: ", e); + logger.error("Failed to validate signature with " + verifier + " error message: " + e.getMessage()); } } return false; @@ -259,7 +274,7 @@ public boolean validateSignature(SignedJWT jwt) { @Override public Map getAllPublicKeys() { - Map pubKeys = new HashMap(); + Map pubKeys = new HashMap<>(); // pull all keys out of the verifiers if we know how for (String keyId : keys.keySet()) { @@ -279,14 +294,14 @@ public Map getAllPublicKeys() { @Override public Collection getAllSigningAlgsSupported() { - Set algs = new HashSet(); + Set algs = new HashSet<>(); for (JWSSigner signer : signers.values()) { - algs.addAll(signer.supportedAlgorithms()); + algs.addAll(signer.supportedJWSAlgorithms()); } for (JWSVerifier verifier : verifiers.values()) { - algs.addAll(verifier.supportedAlgorithms()); + algs.addAll(verifier.supportedJWSAlgorithms()); } return algs; diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/JWKSetCacheService.java b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/JWKSetCacheService.java index 81abdd2f06..8c98005115 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/JWKSetCacheService.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/JWKSetCacheService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.jwt.signer.service.impl; @@ -23,51 +24,56 @@ import java.util.concurrent.TimeUnit; import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.mitre.jose.keystore.JWKSetKeyStore; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; -import org.mitre.jwt.encryption.service.impl.DefaultJwtEncryptionAndDecryptionService; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; +import org.mitre.jwt.encryption.service.impl.DefaultJWTEncryptionAndDecryptionService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.gson.JsonParseException; import com.nimbusds.jose.jwk.JWKSet; /** - * + * * Creates a caching map of JOSE signers/validators and encrypters/decryptors * keyed on the JWK Set URI. Dynamically loads JWK Sets to create the services. - * + * * @author jricher - * + * */ @Service public class JWKSetCacheService { - private static Logger logger = LoggerFactory.getLogger(JWKSetCacheService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JWKSetCacheService.class); // map of jwk set uri -> signing/validation service built on the keys found in that jwk set - private LoadingCache validators; + private LoadingCache validators; // map of jwk set uri -> encryption/decryption service built on the keys found in that jwk set - private LoadingCache encrypters; + private LoadingCache encrypters; public JWKSetCacheService() { this.validators = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) // expires 1 hour after fetch .maximumSize(100) - .build(new JWKSetVerifierFetcher()); + .build(new JWKSetVerifierFetcher(HttpClientBuilder.create().useSystemProperties().build())); this.encrypters = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) // expires 1 hour after fetch .maximumSize(100) - .build(new JWKSetEncryptorFetcher()); + .build(new JWKSetEncryptorFetcher(HttpClientBuilder.create().useSystemProperties().build())); } /** @@ -76,26 +82,20 @@ public JWKSetCacheService() { * @throws ExecutionException * @see com.google.common.cache.Cache#get(java.lang.Object) */ - public JwtSigningAndValidationService getValidator(String jwksUri) { + public JWTSigningAndValidationService getValidator(String jwksUri) { try { return validators.get(jwksUri); - } catch (UncheckedExecutionException ue) { - logger.warn("Couldn't load JWK Set from " + jwksUri, ue); - return null; - } catch (ExecutionException e) { - logger.warn("Couldn't load JWK Set from " + jwksUri, e); + } catch (UncheckedExecutionException | ExecutionException e) { + logger.warn("Couldn't load JWK Set from " + jwksUri + ": " + e.getMessage()); return null; } } - public JwtEncryptionAndDecryptionService getEncrypter(String jwksUri) { + public JWTEncryptionAndDecryptionService getEncrypter(String jwksUri) { try { return encrypters.get(jwksUri); - } catch (UncheckedExecutionException ue) { - logger.warn("Couldn't load JWK Set from " + jwksUri, ue); - return null; - } catch (ExecutionException e) { - logger.warn("Couldn't load JWK Set from " + jwksUri, e); + } catch (UncheckedExecutionException | ExecutionException e) { + logger.warn("Couldn't load JWK Set from " + jwksUri + ": " + e.getMessage()); return null; } } @@ -104,26 +104,28 @@ public JwtEncryptionAndDecryptionService getEncrypter(String jwksUri) { * @author jricher * */ - private class JWKSetVerifierFetcher extends CacheLoader { - private HttpClient httpClient = new SystemDefaultHttpClient(); - private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); - private RestTemplate restTemplate = new RestTemplate(httpFactory); + private class JWKSetVerifierFetcher extends CacheLoader { + private HttpComponentsClientHttpRequestFactory httpFactory; + private RestTemplate restTemplate; + + JWKSetVerifierFetcher(HttpClient httpClient) { + this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + this.restTemplate = new RestTemplate(httpFactory); + } /** * Load the JWK Set and build the appropriate signing service. */ @Override - public JwtSigningAndValidationService load(String key) throws Exception { - + public JWTSigningAndValidationService load(String key) throws Exception { String jsonString = restTemplate.getForObject(key, String.class); JWKSet jwkSet = JWKSet.parse(jsonString); JWKSetKeyStore keyStore = new JWKSetKeyStore(jwkSet); - JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService(keyStore); + JWTSigningAndValidationService service = new DefaultJWTSigningAndValidationService(keyStore); return service; - } } @@ -132,23 +134,32 @@ public JwtSigningAndValidationService load(String key) throws Exception { * @author jricher * */ - private class JWKSetEncryptorFetcher extends CacheLoader { - private HttpClient httpClient = new SystemDefaultHttpClient(); - private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); - private RestTemplate restTemplate = new RestTemplate(httpFactory); + private class JWKSetEncryptorFetcher extends CacheLoader { + private HttpComponentsClientHttpRequestFactory httpFactory; + private RestTemplate restTemplate; + + public JWKSetEncryptorFetcher(HttpClient httpClient) { + this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + this.restTemplate = new RestTemplate(httpFactory); + } + /* (non-Javadoc) * @see com.google.common.cache.CacheLoader#load(java.lang.Object) */ @Override - public JwtEncryptionAndDecryptionService load(String key) throws Exception { - String jsonString = restTemplate.getForObject(key, String.class); - JWKSet jwkSet = JWKSet.parse(jsonString); + public JWTEncryptionAndDecryptionService load(String key) throws Exception { + try { + String jsonString = restTemplate.getForObject(key, String.class); + JWKSet jwkSet = JWKSet.parse(jsonString); - JWKSetKeyStore keyStore = new JWKSetKeyStore(jwkSet); + JWKSetKeyStore keyStore = new JWKSetKeyStore(jwkSet); - JwtEncryptionAndDecryptionService service = new DefaultJwtEncryptionAndDecryptionService(keyStore); + JWTEncryptionAndDecryptionService service = new DefaultJWTEncryptionAndDecryptionService(keyStore); - return service; + return service; + } catch (JsonParseException | RestClientException e) { + throw new IllegalArgumentException("Unable to load JWK Set"); + } } } diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/SymmetricCacheService.java b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/SymmetricKeyJWTValidatorCacheService.java similarity index 76% rename from openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/SymmetricCacheService.java rename to openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/SymmetricKeyJWTValidatorCacheService.java index 843ecad871..817cffefce 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/SymmetricCacheService.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/SymmetricKeyJWTValidatorCacheService.java @@ -1,6 +1,5 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +21,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import com.nimbusds.jose.Algorithm; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; import org.mitre.oauth2.model.ClientDetailsEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,25 +34,28 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.UncheckedExecutionException; import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.util.Base64URL; /** * Creates and caches symmetrical validators for clients based on client secrets. - * + * * @author jricher * */ @Service -public class SymmetricCacheService { +public class SymmetricKeyJWTValidatorCacheService { - private static Logger logger = LoggerFactory.getLogger(SymmetricCacheService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(SymmetricKeyJWTValidatorCacheService.class); - private LoadingCache validators; + private LoadingCache validators; - public SymmetricCacheService() { + public SymmetricKeyJWTValidatorCacheService() { validators = CacheBuilder.newBuilder() .expireAfterAccess(24, TimeUnit.HOURS) .maximumSize(100) @@ -64,11 +65,11 @@ public SymmetricCacheService() { /** * Create a symmetric signing and validation service for the given client - * + * * @param client * @return */ - public JwtSigningAndValidationService getSymmetricValidtor(ClientDetailsEntity client) { + public JWTSigningAndValidationService getSymmetricValidtor(ClientDetailsEntity client) { if (client == null) { logger.error("Couldn't create symmetric validator for null client"); @@ -92,22 +93,22 @@ public JwtSigningAndValidationService getSymmetricValidtor(ClientDetailsEntity c } - public class SymmetricValidatorBuilder extends CacheLoader { + public class SymmetricValidatorBuilder extends CacheLoader { @Override - public JwtSigningAndValidationService load(String key) throws Exception { + public JWTSigningAndValidationService load(String key) throws Exception { try { String id = "SYMMETRIC-KEY"; - - JWK jwk = new OctetSequenceKey(Base64URL.encode(key), KeyUse.SIGNATURE, null, null, id, null, null, null); + JWK jwk = new OctetSequenceKey.Builder(Base64URL.encode(key)) + .keyUse(KeyUse.SIGNATURE) + .keyID(id) + .build(); Map keys = ImmutableMap.of(id, jwk); - JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService(keys); + JWTSigningAndValidationService service = new DefaultJWTSigningAndValidationService(keys); return service; - } catch (NoSuchAlgorithmException e) { - logger.error("Couldn't create symmetric validator for client", e); - } catch (InvalidKeySpecException e) { + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { logger.error("Couldn't create symmetric validator for client", e); } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/exception/DeviceCodeCreationException.java b/openid-connect-common/src/main/java/org/mitre/oauth2/exception/DeviceCodeCreationException.java new file mode 100644 index 0000000000..9924505071 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/exception/DeviceCodeCreationException.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.exception; + +/** + * @author jricher + * + */ +public class DeviceCodeCreationException extends Exception { + + private static final long serialVersionUID = 8078568710169208466L; + + private String error; + + public DeviceCodeCreationException(String error, String message) { + super(message); + this.error = error; + } + + /** + * @return the error + */ + public String getError() { + return error; + } + + /** + * @param error the error to set + */ + public void setError(String error) { + this.error = error; + } + + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthenticationHolderEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthenticationHolderEntity.java index cfe901f3e3..28accd47e7 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthenticationHolderEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthenticationHolderEntity.java @@ -1,48 +1,89 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; -import javax.persistence.Lob; +import javax.persistence.JoinColumn; +import javax.persistence.MapKeyColumn; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; import javax.persistence.Table; +import javax.persistence.Transient; +import org.mitre.oauth2.model.convert.SerializableStringConverter; +import org.mitre.oauth2.model.convert.SimpleGrantedAuthorityStringConverter; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; @Entity @Table(name = "authentication_holder") @NamedQueries ({ - @NamedQuery(name = "AuthenticationHolderEntity.getByAuthentication", query = "select a from AuthenticationHolderEntity a where a.authentication = :authentication"), - @NamedQuery(name = "AuthenticationHolderEntity.getUnusedAuthenticationHolders", query = "select a from AuthenticationHolderEntity a where a.id not in (select t.authenticationHolder.id from OAuth2AccessTokenEntity t) and a.id not in (select r.authenticationHolder.id from OAuth2RefreshTokenEntity r)") + @NamedQuery(name = AuthenticationHolderEntity.QUERY_ALL, query = "select a from AuthenticationHolderEntity a"), + @NamedQuery(name = AuthenticationHolderEntity.QUERY_GET_UNUSED, query = "select a from AuthenticationHolderEntity a where " + + "a.id not in (select t.authenticationHolder.id from OAuth2AccessTokenEntity t) and " + + "a.id not in (select r.authenticationHolder.id from OAuth2RefreshTokenEntity r) and " + + "a.id not in (select c.authenticationHolder.id from AuthorizationCodeEntity c)") }) public class AuthenticationHolderEntity { + public static final String QUERY_GET_UNUSED = "AuthenticationHolderEntity.getUnusedAuthenticationHolders"; + public static final String QUERY_ALL = "AuthenticationHolderEntity.getAll"; + private Long id; - private Long ownerId; + private SavedUserAuthentication userAuth; + + private Collection authorities; + + private Set resourceIds; + + private boolean approved; + + private String redirectUri; + + private Set responseTypes; - private OAuth2Authentication authentication; + private Map extensions; + + private String clientId; + + private Set scope; + + private Map requestParameters; public AuthenticationHolderEntity() { @@ -59,25 +100,226 @@ public void setId(Long id) { this.id = id; } + @Transient + public OAuth2Authentication getAuthentication() { + // TODO: memoize this + return new OAuth2Authentication(createOAuth2Request(), getUserAuth()); + } + + /** + * @return + */ + private OAuth2Request createOAuth2Request() { + return new OAuth2Request(requestParameters, clientId, authorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions); + } + + public void setAuthentication(OAuth2Authentication authentication) { + + // pull apart the request and save its bits + OAuth2Request o2Request = authentication.getOAuth2Request(); + setAuthorities(o2Request.getAuthorities() == null ? null : new HashSet<>(o2Request.getAuthorities())); + setClientId(o2Request.getClientId()); + setExtensions(o2Request.getExtensions() == null ? null : new HashMap<>(o2Request.getExtensions())); + setRedirectUri(o2Request.getRedirectUri()); + setRequestParameters(o2Request.getRequestParameters() == null ? null : new HashMap<>(o2Request.getRequestParameters())); + setResourceIds(o2Request.getResourceIds() == null ? null : new HashSet<>(o2Request.getResourceIds())); + setResponseTypes(o2Request.getResponseTypes() == null ? null : new HashSet<>(o2Request.getResponseTypes())); + setScope(o2Request.getScope() == null ? null : new HashSet<>(o2Request.getScope())); + setApproved(o2Request.isApproved()); + + if (authentication.getUserAuthentication() != null) { + this.userAuth = new SavedUserAuthentication(authentication.getUserAuthentication()); + } else { + this.userAuth = null; + } + } + + /** + * @return the userAuth + */ + @OneToOne(cascade=CascadeType.ALL) + @JoinColumn(name = "user_auth_id") + public SavedUserAuthentication getUserAuth() { + return userAuth; + } + + /** + * @param userAuth the userAuth to set + */ + public void setUserAuth(SavedUserAuthentication userAuth) { + this.userAuth = userAuth; + } + + /** + * @return the authorities + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authentication_holder_authority", + joinColumns=@JoinColumn(name="owner_id") + ) + @Convert(converter = SimpleGrantedAuthorityStringConverter.class) + @Column(name="authority") + public Collection getAuthorities() { + return authorities; + } + + /** + * @param authorities the authorities to set + */ + public void setAuthorities(Collection authorities) { + this.authorities = authorities; + } + + /** + * @return the resourceIds + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authentication_holder_resource_id", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="resource_id") + public Set getResourceIds() { + return resourceIds; + } + + /** + * @param resourceIds the resourceIds to set + */ + public void setResourceIds(Set resourceIds) { + this.resourceIds = resourceIds; + } + + /** + * @return the approved + */ @Basic - @Column(name = "owner_id") - public Long getOwnerId() { - return ownerId; + @Column(name="approved") + public boolean isApproved() { + return approved; } - public void setOwnerId(Long owner_id) { - this.ownerId = owner_id; + /** + * @param approved the approved to set + */ + public void setApproved(boolean approved) { + this.approved = approved; } - @Lob - @Basic(fetch=FetchType.LAZY) - @Column(name = "authentication") - public OAuth2Authentication getAuthentication() { - return authentication; + /** + * @return the redirectUri + */ + @Basic + @Column(name="redirect_uri") + public String getRedirectUri() { + return redirectUri; } - public void setAuthentication(OAuth2Authentication authentication) { - this.authentication = authentication; + /** + * @param redirectUri the redirectUri to set + */ + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + /** + * @return the responseTypes + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authentication_holder_response_type", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="response_type") + public Set getResponseTypes() { + return responseTypes; + } + + /** + * @param responseTypes the responseTypes to set + */ + public void setResponseTypes(Set responseTypes) { + this.responseTypes = responseTypes; + } + + /** + * @return the extensions + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authentication_holder_extension", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="val") + @MapKeyColumn(name="extension") + @Convert(converter=SerializableStringConverter.class) + public Map getExtensions() { + return extensions; + } + + /** + * @param extensions the extensions to set + */ + public void setExtensions(Map extensions) { + this.extensions = extensions; + } + + /** + * @return the clientId + */ + @Basic + @Column(name="client_id") + public String getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * @return the scope + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authentication_holder_scope", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="scope") + public Set getScope() { + return scope; + } + + /** + * @param scope the scope to set + */ + public void setScope(Set scope) { + this.scope = scope; + } + + /** + * @return the requestParameters + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authentication_holder_request_parameter", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="val") + @MapKeyColumn(name="param") + public Map getRequestParameters() { + return requestParameters; + } + + /** + * @param requestParameters the requestParameters to set + */ + public void setRequestParameters(Map requestParameters) { + this.requestParameters = requestParameters; } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthorizationCodeEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthorizationCodeEntity.java index 7411ba5e53..385f467685 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthorizationCodeEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/AuthorizationCodeEntity.java @@ -1,53 +1,63 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; +import java.util.Date; + import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; -import javax.persistence.Lob; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; - -import org.springframework.security.oauth2.provider.OAuth2Authentication; +import javax.persistence.Temporal; /** * Entity class for authorization codes - * + * * @author aanganes * */ @Entity @Table(name = "authorization_code") @NamedQueries({ - @NamedQuery(name = "AuthorizationCodeEntity.getByValue", query = "select a from AuthorizationCodeEntity a where a.code = :code") + @NamedQuery(name = AuthorizationCodeEntity.QUERY_BY_VALUE, query = "select a from AuthorizationCodeEntity a where a.code = :code"), + @NamedQuery(name = AuthorizationCodeEntity.QUERY_EXPIRATION_BY_DATE, query = "select a from AuthorizationCodeEntity a where a.expiration <= :" + AuthorizationCodeEntity.PARAM_DATE) }) public class AuthorizationCodeEntity { + public static final String QUERY_BY_VALUE = "AuthorizationCodeEntity.getByValue"; + public static final String QUERY_EXPIRATION_BY_DATE = "AuthorizationCodeEntity.expirationByDate"; + + public static final String PARAM_DATE = "date"; + private Long id; private String code; - private OAuth2Authentication authentication; + private AuthenticationHolderEntity authenticationHolder; + + private Date expiration; /** * Default constructor. @@ -58,13 +68,14 @@ public AuthorizationCodeEntity() { /** * Create a new AuthorizationCodeEntity with the given code and AuthorizationRequestHolder. - * + * * @param code the authorization code * @param authRequest the AuthoriztionRequestHolder associated with the original code request */ - public AuthorizationCodeEntity(String code, OAuth2Authentication authRequest) { + public AuthorizationCodeEntity(String code, AuthenticationHolderEntity authenticationHolder, Date expiration) { this.code = code; - this.authentication = authRequest; + this.authenticationHolder = authenticationHolder; + this.expiration = expiration; } /** @@ -101,20 +112,30 @@ public void setCode(String code) { } /** + * The authentication in place when this token was created. * @return the authentication */ - @Lob - @Basic(fetch=FetchType.EAGER) - @Column(name="authentication") - public OAuth2Authentication getAuthentication() { - return authentication; + @ManyToOne + @JoinColumn(name = "auth_holder_id") + public AuthenticationHolderEntity getAuthenticationHolder() { + return authenticationHolder; } /** * @param authentication the authentication to set */ - public void setAuthentication(OAuth2Authentication authentication) { - this.authentication = authentication; + public void setAuthenticationHolder(AuthenticationHolderEntity authenticationHolder) { + this.authenticationHolder = authenticationHolder; } + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + @Column(name = "expiration") + public Date getExpiration() { + return expiration; + } + + public void setExpiration(Date expiration) { + this.expiration = expiration; + } } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java index 252a41cada..c161c07970 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; @@ -25,13 +26,11 @@ import java.util.Map; import java.util.Set; -import javax.persistence.AttributeOverride; -import javax.persistence.AttributeOverrides; import javax.persistence.Basic; import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.ElementCollection; -import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -49,31 +48,39 @@ import javax.persistence.TemporalType; import javax.persistence.Transient; -import org.mitre.jose.JWEAlgorithmEmbed; -import org.mitre.jose.JWEEncryptionMethodEmbed; -import org.mitre.jose.JWSAlgorithmEmbed; +import org.mitre.oauth2.model.convert.JWEAlgorithmStringConverter; +import org.mitre.oauth2.model.convert.JWEEncryptionMethodStringConverter; +import org.mitre.oauth2.model.convert.JWKSetStringConverter; +import org.mitre.oauth2.model.convert.JWSAlgorithmStringConverter; +import org.mitre.oauth2.model.convert.JWTStringConverter; +import org.mitre.oauth2.model.convert.PKCEAlgorithmStringConverter; +import org.mitre.oauth2.model.convert.SimpleGrantedAuthorityStringConverter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.provider.ClientDetails; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWT; /** * @author jricher - * + * */ @Entity @Table(name = "client_details") @NamedQueries({ - @NamedQuery(name = "ClientDetailsEntity.findAll", query = "SELECT c FROM ClientDetailsEntity c"), - @NamedQuery(name = "ClientDetailsEntity.getByClientId", query = "select c from ClientDetailsEntity c where c.clientId = :clientId") + @NamedQuery(name = ClientDetailsEntity.QUERY_ALL, query = "SELECT c FROM ClientDetailsEntity c"), + @NamedQuery(name = ClientDetailsEntity.QUERY_BY_CLIENT_ID, query = "select c from ClientDetailsEntity c where c.clientId = :" + ClientDetailsEntity.PARAM_CLIENT_ID) }) public class ClientDetailsEntity implements ClientDetails { - /** - * - */ + public static final String QUERY_BY_CLIENT_ID = "ClientDetailsEntity.getByClientId"; + public static final String QUERY_ALL = "ClientDetailsEntity.findAll"; + + public static final String PARAM_CLIENT_ID = "clientId"; + private static final int DEFAULT_ID_TOKEN_VALIDITY_SECONDS = 600; private static final long serialVersionUID = -1617727085733786296L; @@ -83,51 +90,54 @@ public class ClientDetailsEntity implements ClientDetails { /** Fields from the OAuth2 Dynamic Registration Specification */ private String clientId = null; // client_id private String clientSecret = null; // client_secret - private Set redirectUris = new HashSet(); // redirect_uris + private Set redirectUris = new HashSet<>(); // redirect_uris private String clientName; // client_name private String clientUri; // client_uri private String logoUri; // logo_uri private Set contacts; // contacts private String tosUri; // tos_uri private AuthMethod tokenEndpointAuthMethod = AuthMethod.SECRET_BASIC; // token_endpoint_auth_method - private Set scope = new HashSet(); // scope - private Set grantTypes = new HashSet(); // grant_types - private Set responseTypes = new HashSet(); // response_types + private Set scope = new HashSet<>(); // scope + private Set grantTypes = new HashSet<>(); // grant_types + private Set responseTypes = new HashSet<>(); // response_types private String policyUri; - private String jwksUri; + private String jwksUri; // URI pointer to keys + private JWKSet jwks; // public key stored by value + private String softwareId; + private String softwareVersion; /** Fields from OIDC Client Registration Specification **/ private AppType applicationType; // application_type private String sectorIdentifierUri; // sector_identifier_uri private SubjectType subjectType; // subject_type - private JWSAlgorithmEmbed requestObjectSigningAlg = null; // request_object_signing_alg + private JWSAlgorithm requestObjectSigningAlg = null; // request_object_signing_alg - private JWSAlgorithmEmbed userInfoSignedResponseAlg = null; // user_info_signed_response_alg - private JWEAlgorithmEmbed userInfoEncryptedResponseAlg = null; // user_info_encrypted_response_alg - private JWEEncryptionMethodEmbed userInfoEncryptedResponseEnc = null; // user_info_encrypted_response_enc + private JWSAlgorithm userInfoSignedResponseAlg = null; // user_info_signed_response_alg + private JWEAlgorithm userInfoEncryptedResponseAlg = null; // user_info_encrypted_response_alg + private EncryptionMethod userInfoEncryptedResponseEnc = null; // user_info_encrypted_response_enc - private JWSAlgorithmEmbed idTokenSignedResponseAlg = null; // id_token_signed_response_alg - private JWEAlgorithmEmbed idTokenEncryptedResponseAlg = null; // id_token_encrypted_response_alg - private JWEEncryptionMethodEmbed idTokenEncryptedResponseEnc = null; // id_token_encrypted_response_enc + private JWSAlgorithm idTokenSignedResponseAlg = null; // id_token_signed_response_alg + private JWEAlgorithm idTokenEncryptedResponseAlg = null; // id_token_encrypted_response_alg + private EncryptionMethod idTokenEncryptedResponseEnc = null; // id_token_encrypted_response_enc - private JWSAlgorithmEmbed tokenEndpointAuthSigningAlg = null; // token_endpoint_auth_signing_alg + private JWSAlgorithm tokenEndpointAuthSigningAlg = null; // token_endpoint_auth_signing_alg private Integer defaultMaxAge; // default_max_age private Boolean requireAuthTime; // require_auth_time private Set defaultACRvalues; // default_acr_values private String initiateLoginUri; // initiate_login_uri - private String postLogoutRedirectUri; // post_logout_redirect_uri + private Set postLogoutRedirectUris; // post_logout_redirect_uris private Set requestUris; // request_uris /** Fields to support the ClientDetails interface **/ - private Set authorities = new HashSet(); + private Set authorities = new HashSet<>(); private Integer accessTokenValiditySeconds = 0; // in seconds private Integer refreshTokenValiditySeconds = 0; // in seconds - private Set resourceIds = new HashSet(); - private Map additionalInformation = new HashMap(); + private Set resourceIds = new HashSet<>(); + private Map additionalInformation = new HashMap<>(); /** Our own fields **/ private String clientDescription = ""; // human-readable description @@ -136,6 +146,17 @@ public class ClientDetailsEntity implements ClientDetails { private boolean allowIntrospection = false; // do we let this client call the introspection endpoint? private Integer idTokenValiditySeconds; //timeout for id tokens private Date createdAt; // time the client was created + private boolean clearAccessTokensOnRefresh = true; // do we clear access tokens on refresh? + private Integer deviceCodeValiditySeconds; // timeout for device codes + + /** fields for UMA */ + private Set claimsRedirectUris; + + /** Software statement **/ + private JWT softwareStatement; + + /** PKCE **/ + private PKCEAlgorithm codeChallengeMethod; public enum AuthMethod { SECRET_POST("client_secret_post"), @@ -147,7 +168,7 @@ public enum AuthMethod { private final String value; // map to aid reverse lookup - private static final Map lookup = new HashMap(); + private static final Map lookup = new HashMap<>(); static { for (AuthMethod a : AuthMethod.values()) { lookup.put(a.getValue(), a); @@ -173,7 +194,7 @@ public enum AppType { private final String value; // map to aid reverse lookup - private static final Map lookup = new HashMap(); + private static final Map lookup = new HashMap<>(); static { for (AppType a : AppType.values()) { lookup.put(a.getValue(), a); @@ -199,7 +220,7 @@ public enum SubjectType { private final String value; // map to aid reverse lookup - private static final Map lookup = new HashMap(); + private static final Map lookup = new HashMap<>(); static { for (SubjectType u : SubjectType.values()) { lookup.put(u.getValue(), u); @@ -246,7 +267,7 @@ public Long getId() { } /** - * + * * @param id the id to set */ public void setId(Long id) { @@ -293,7 +314,7 @@ public void setReuseRefreshToken(boolean reuseRefreshToken) { /** * Number of seconds ID token is valid for. MUST be a positive integer, can not be null. - * + * * @return the idTokenValiditySeconds */ @Basic @@ -346,15 +367,15 @@ public void setAllowIntrospection(boolean allowIntrospection) { } /** - * + * */ @Override @Transient public boolean isSecretRequired() { if (getTokenEndpointAuthMethod() != null && (getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) || - getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) || - getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT))) { + getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) || + getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT))) { return true; } else { return false; @@ -450,6 +471,7 @@ public void setGrantTypes(Set grantTypes) { * passthrough for SECOAUTH api */ @Override + @Transient public Set getAuthorizedGrantTypes() { return getGrantTypes(); } @@ -463,6 +485,7 @@ public Set getAuthorizedGrantTypes() { joinColumns=@JoinColumn(name="owner_id") ) @Override + @Convert(converter = SimpleGrantedAuthorityStringConverter.class) @Column(name="authority") public Set getAuthorities() { return authorities; @@ -557,9 +580,9 @@ public void setResourceIds(Set resourceIds) { /** * This library does not make use of this field, so it is not * stored using our persistence layer. - * + * * However, it's somehow required by SECOUATH. - * + * * @return an empty map */ @Override @@ -687,6 +710,23 @@ public void setJwksUri(String jwksUri) { this.jwksUri = jwksUri; } + /** + * @return the jwks + */ + @Basic + @Column(name="jwks") + @Convert(converter = JWKSetStringConverter.class) + public JWKSet getJwks() { + return jwks; + } + + /** + * @param jwks the jwks to set + */ + public void setJwks(JWKSet jwks) { + this.jwks = jwks; + } + @Basic @Column(name="sector_identifier_uri") public String getSectorIdentifierUri() { @@ -697,212 +737,94 @@ public void setSectorIdentifierUri(String sectorIdentifierUri) { this.sectorIdentifierUri = sectorIdentifierUri; } - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="request_object_signing_alg")) - }) - public JWSAlgorithmEmbed getRequestObjectSigningAlgEmbed() { - return requestObjectSigningAlg; - } - - public void setRequestObjectSigningAlgEmbed(JWSAlgorithmEmbed requestObjectSigningAlg) { - this.requestObjectSigningAlg = requestObjectSigningAlg; - } - - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="user_info_signed_response_alg")) - }) - public JWSAlgorithmEmbed getUserInfoSignedResponseAlgEmbed() { - return userInfoSignedResponseAlg; - } - - public void setUserInfoSignedResponseAlgEmbed(JWSAlgorithmEmbed userInfoSignedResponseAlg) { - this.userInfoSignedResponseAlg = userInfoSignedResponseAlg; - } - - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="user_info_encrypted_response_alg")) - }) - public JWEAlgorithmEmbed getUserInfoEncryptedResponseAlgEmbed() { - return userInfoEncryptedResponseAlg; - } - - public void setUserInfoEncryptedResponseAlgEmbed(JWEAlgorithmEmbed userInfoEncryptedResponseAlg) { - this.userInfoEncryptedResponseAlg = userInfoEncryptedResponseAlg; - } - - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="user_info_encrypted_response_enc")) - }) - public JWEEncryptionMethodEmbed getUserInfoEncryptedResponseEncEmbed() { - return userInfoEncryptedResponseEnc; - } - - public void setUserInfoEncryptedResponseEncEmbed(JWEEncryptionMethodEmbed userInfoEncryptedResponseEnc) { - this.userInfoEncryptedResponseEnc = userInfoEncryptedResponseEnc; - } - - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="id_token_signed_response_alg")) - }) - public JWSAlgorithmEmbed getIdTokenSignedResponseAlgEmbed() { - return idTokenSignedResponseAlg; - } - - public void setIdTokenSignedResponseAlgEmbed(JWSAlgorithmEmbed idTokenSignedResponseAlg) { - this.idTokenSignedResponseAlg = idTokenSignedResponseAlg; - } - - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="id_token_encrypted_response_alg")) - }) - public JWEAlgorithmEmbed getIdTokenEncryptedResponseAlgEmbed() { - return idTokenEncryptedResponseAlg; - } - - public void setIdTokenEncryptedResponseAlgEmbed(JWEAlgorithmEmbed idTokenEncryptedResponseAlg) { - this.idTokenEncryptedResponseAlg = idTokenEncryptedResponseAlg; - } - - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="id_token_encrypted_response_enc")) - }) - public JWEEncryptionMethodEmbed getIdTokenEncryptedResponseEncEmbed() { - return idTokenEncryptedResponseEnc; - } - - public void setIdTokenEncryptedResponseEncEmbed(JWEEncryptionMethodEmbed idTokenEncryptedResponseEnc) { - this.idTokenEncryptedResponseEnc = idTokenEncryptedResponseEnc; - } - - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "algorithmName", column=@Column(name="token_endpoint_auth_signing_alg")) - }) - public JWSAlgorithmEmbed getTokenEndpointAuthSigningAlgEmbed() { - return tokenEndpointAuthSigningAlg; - } - - public void setTokenEndpointAuthSigningAlgEmbed(JWSAlgorithmEmbed tokenEndpointAuthSigningAlgEmbed) { - this.tokenEndpointAuthSigningAlg = tokenEndpointAuthSigningAlgEmbed; - } - - // - // Transient passthrough methods for JOSE elements - // - - @Transient + @Basic + @Column(name = "request_object_signing_alg") + @Convert(converter = JWSAlgorithmStringConverter.class) public JWSAlgorithm getRequestObjectSigningAlg() { - if (requestObjectSigningAlg != null) { - return requestObjectSigningAlg.getAlgorithm(); - } else { - return null; - } + return requestObjectSigningAlg; } public void setRequestObjectSigningAlg(JWSAlgorithm requestObjectSigningAlg) { - this.requestObjectSigningAlg = new JWSAlgorithmEmbed(requestObjectSigningAlg); + this.requestObjectSigningAlg = requestObjectSigningAlg; } - @Transient + @Basic + @Column(name = "user_info_signed_response_alg") + @Convert(converter = JWSAlgorithmStringConverter.class) public JWSAlgorithm getUserInfoSignedResponseAlg() { - if (userInfoSignedResponseAlg != null) { - return userInfoSignedResponseAlg.getAlgorithm(); - } else { - return null; - } + return userInfoSignedResponseAlg; } public void setUserInfoSignedResponseAlg(JWSAlgorithm userInfoSignedResponseAlg) { - this.userInfoSignedResponseAlg = new JWSAlgorithmEmbed(userInfoSignedResponseAlg); + this.userInfoSignedResponseAlg = userInfoSignedResponseAlg; } - @Transient + @Basic + @Column(name = "user_info_encrypted_response_alg") + @Convert(converter = JWEAlgorithmStringConverter.class) public JWEAlgorithm getUserInfoEncryptedResponseAlg() { - if (userInfoEncryptedResponseAlg != null) { - return userInfoEncryptedResponseAlg.getAlgorithm(); - } else { - return null; - } + return userInfoEncryptedResponseAlg; } public void setUserInfoEncryptedResponseAlg(JWEAlgorithm userInfoEncryptedResponseAlg) { - this.userInfoEncryptedResponseAlg = new JWEAlgorithmEmbed(userInfoEncryptedResponseAlg); + this.userInfoEncryptedResponseAlg = userInfoEncryptedResponseAlg; } - @Transient + @Basic + @Column(name = "user_info_encrypted_response_enc") + @Convert(converter = JWEEncryptionMethodStringConverter.class) public EncryptionMethod getUserInfoEncryptedResponseEnc() { - if (userInfoEncryptedResponseEnc != null) { - return userInfoEncryptedResponseEnc.getAlgorithm(); - } else { - return null; - } + return userInfoEncryptedResponseEnc; } public void setUserInfoEncryptedResponseEnc(EncryptionMethod userInfoEncryptedResponseEnc) { - this.userInfoEncryptedResponseEnc = new JWEEncryptionMethodEmbed(userInfoEncryptedResponseEnc); + this.userInfoEncryptedResponseEnc = userInfoEncryptedResponseEnc; } - @Transient + @Basic + @Column(name="id_token_signed_response_alg") + @Convert(converter = JWSAlgorithmStringConverter.class) public JWSAlgorithm getIdTokenSignedResponseAlg() { - if (idTokenSignedResponseAlg != null) { - return idTokenSignedResponseAlg.getAlgorithm(); - } else { - return null; - } + return idTokenSignedResponseAlg; } public void setIdTokenSignedResponseAlg(JWSAlgorithm idTokenSignedResponseAlg) { - this.idTokenSignedResponseAlg = new JWSAlgorithmEmbed(idTokenSignedResponseAlg); + this.idTokenSignedResponseAlg = idTokenSignedResponseAlg; } - @Transient + @Basic + @Column(name = "id_token_encrypted_response_alg") + @Convert(converter = JWEAlgorithmStringConverter.class) public JWEAlgorithm getIdTokenEncryptedResponseAlg() { - if (idTokenEncryptedResponseAlg != null) { - return idTokenEncryptedResponseAlg.getAlgorithm(); - } else { - return null; - } + return idTokenEncryptedResponseAlg; } public void setIdTokenEncryptedResponseAlg(JWEAlgorithm idTokenEncryptedResponseAlg) { - this.idTokenEncryptedResponseAlg = new JWEAlgorithmEmbed(idTokenEncryptedResponseAlg); + this.idTokenEncryptedResponseAlg = idTokenEncryptedResponseAlg; } - @Transient + @Basic + @Column(name = "id_token_encrypted_response_enc") + @Convert(converter = JWEEncryptionMethodStringConverter.class) public EncryptionMethod getIdTokenEncryptedResponseEnc() { - if (idTokenEncryptedResponseEnc != null) { - return idTokenEncryptedResponseEnc.getAlgorithm(); - } else { - return null; - } + return idTokenEncryptedResponseEnc; } public void setIdTokenEncryptedResponseEnc(EncryptionMethod idTokenEncryptedResponseEnc) { - this.idTokenEncryptedResponseEnc = new JWEEncryptionMethodEmbed(idTokenEncryptedResponseEnc); + this.idTokenEncryptedResponseEnc = idTokenEncryptedResponseEnc; } - @Transient + @Basic + @Column(name="token_endpoint_auth_signing_alg") + @Convert(converter = JWSAlgorithmStringConverter.class) public JWSAlgorithm getTokenEndpointAuthSigningAlg() { - if (tokenEndpointAuthSigningAlg != null) { - return tokenEndpointAuthSigningAlg.getAlgorithm(); - } else { - return null; - } + return tokenEndpointAuthSigningAlg; } public void setTokenEndpointAuthSigningAlg(JWSAlgorithm tokenEndpointAuthSigningAlg) { - this.tokenEndpointAuthSigningAlg = new JWSAlgorithmEmbed(tokenEndpointAuthSigningAlg); + this.tokenEndpointAuthSigningAlg = tokenEndpointAuthSigningAlg; } - // END Transient JOSE methods - @Basic @Column(name="default_max_age") public Integer getDefaultMaxAge() { @@ -982,17 +904,21 @@ public void setInitiateLoginUri(String initiateLoginUri) { /** * @return the postLogoutRedirectUri */ - @Basic + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="client_post_logout_redirect_uri", + joinColumns=@JoinColumn(name="owner_id") + ) @Column(name="post_logout_redirect_uri") - public String getPostLogoutRedirectUri() { - return postLogoutRedirectUri; + public Set getPostLogoutRedirectUris() { + return postLogoutRedirectUris; } /** * @param postLogoutRedirectUri the postLogoutRedirectUri to set */ - public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { - this.postLogoutRedirectUri = postLogoutRedirectUri; + public void setPostLogoutRedirectUris(Set postLogoutRedirectUri) { + this.postLogoutRedirectUris = postLogoutRedirectUri; } /** @@ -1039,4 +965,122 @@ public boolean isAutoApprove(String scope) { return false; } + /** + * @return the clearAccessTokensOnRefresh + */ + @Basic + @Column(name = "clear_access_tokens_on_refresh") + public boolean isClearAccessTokensOnRefresh() { + return clearAccessTokensOnRefresh; + } + + /** + * @param clearAccessTokensOnRefresh the clearAccessTokensOnRefresh to set + */ + public void setClearAccessTokensOnRefresh(boolean clearAccessTokensOnRefresh) { + this.clearAccessTokensOnRefresh = clearAccessTokensOnRefresh; + } + + /** + * @return the claimsRedirectUris + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="client_claims_redirect_uri", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="redirect_uri") + public Set getClaimsRedirectUris() { + return claimsRedirectUris; + } + + /** + * @param claimsRedirectUris the claimsRedirectUris to set + */ + public void setClaimsRedirectUris(Set claimsRedirectUris) { + this.claimsRedirectUris = claimsRedirectUris; + } + + /** + * @return the softwareStatement + */ + @Basic + @Column(name = "software_statement") + @Convert(converter = JWTStringConverter.class) + public JWT getSoftwareStatement() { + return softwareStatement; + } + + /** + * @param softwareStatement the softwareStatement to set + */ + public void setSoftwareStatement(JWT softwareStatement) { + this.softwareStatement = softwareStatement; + } + + /** + * @return the codeChallengeMethod + */ + @Basic + @Column(name = "code_challenge_method") + @Convert(converter = PKCEAlgorithmStringConverter.class) + public PKCEAlgorithm getCodeChallengeMethod() { + return codeChallengeMethod; + } + + /** + * @param codeChallengeMethod the codeChallengeMethod to set + */ + public void setCodeChallengeMethod(PKCEAlgorithm codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + } + + /** + * @return the deviceCodeValiditySeconds + */ + @Basic + @Column(name="device_code_validity_seconds") + public Integer getDeviceCodeValiditySeconds() { + return deviceCodeValiditySeconds; + } + + /** + * @param deviceCodeValiditySeconds the deviceCodeValiditySeconds to set + */ + public void setDeviceCodeValiditySeconds(Integer deviceCodeValiditySeconds) { + this.deviceCodeValiditySeconds = deviceCodeValiditySeconds; + } + + /** + * @return the softwareId + */ + @Basic + @Column(name="software_id") + public String getSoftwareId() { + return softwareId; + } + + /** + * @param softwareId the softwareId to set + */ + public void setSoftwareId(String softwareId) { + this.softwareId = softwareId; + } + + /** + * @return the softwareVersion + */ + @Basic + @Column(name="software_version") + public String getSoftwareVersion() { + return softwareVersion; + } + + /** + * @param softwareVersion the softwareVersion to set + */ + public void setSoftwareVersion(String softwareVersion) { + this.softwareVersion = softwareVersion; + } + } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/DeviceCode.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/DeviceCode.java new file mode 100644 index 0000000000..c15a95fe11 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/DeviceCode.java @@ -0,0 +1,234 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model; + +import java.util.Date; +import java.util.Map; +import java.util.Set; + +import javax.persistence.Basic; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapKeyColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Temporal; + +/** + * @author jricher + * + */ +@Entity +@Table(name = "device_code") +@NamedQueries({ + @NamedQuery(name = DeviceCode.QUERY_BY_USER_CODE, query = "select d from DeviceCode d where d.userCode = :" + DeviceCode.PARAM_USER_CODE), + @NamedQuery(name = DeviceCode.QUERY_BY_DEVICE_CODE, query = "select d from DeviceCode d where d.deviceCode = :" + DeviceCode.PARAM_DEVICE_CODE), + @NamedQuery(name = DeviceCode.QUERY_EXPIRED_BY_DATE, query = "select d from DeviceCode d where d.expiration <= :" + DeviceCode.PARAM_DATE) +}) +public class DeviceCode { + + public static final String QUERY_BY_USER_CODE = "DeviceCode.queryByUserCode"; + public static final String QUERY_BY_DEVICE_CODE = "DeviceCode.queryByDeviceCode"; + public static final String QUERY_EXPIRED_BY_DATE = "DeviceCode.queryExpiredByDate"; + + public static final String PARAM_USER_CODE = "userCode"; + public static final String PARAM_DEVICE_CODE = "deviceCode"; + public static final String PARAM_DATE = "date"; + + private Long id; + private String deviceCode; + private String userCode; + private Set scope; + private Date expiration; + private String clientId; + private Map requestParameters; + private boolean approved; + private AuthenticationHolderEntity authenticationHolder; + + public DeviceCode() { + + } + + public DeviceCode(String deviceCode, String userCode, Set scope, String clientId, Map params) { + this.deviceCode = deviceCode; + this.userCode = userCode; + this.scope = scope; + this.clientId = clientId; + this.requestParameters = params; + } + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the deviceCode + */ + @Basic + @Column(name = "device_code") + public String getDeviceCode() { + return deviceCode; + } + + /** + * @param deviceCode the deviceCode to set + */ + public void setDeviceCode(String deviceCode) { + this.deviceCode = deviceCode; + } + + /** + * @return the userCode + */ + @Basic + @Column(name = "user_code") + public String getUserCode() { + return userCode; + } + + /** + * @param userCode the userCode to set + */ + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + /** + * @return the scope + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="device_code_scope", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="scope") + public Set getScope() { + return scope; + } + + /** + * @param scope the scope to set + */ + public void setScope(Set scope) { + this.scope = scope; + } + + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + @Column(name = "expiration") + public Date getExpiration() { + return expiration; + } + + public void setExpiration(Date expiration) { + this.expiration = expiration; + } + + /** + * @return the clientId + */ + @Basic + @Column(name = "client_id") + public String getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * @return the params + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="device_code_request_parameter", + joinColumns=@JoinColumn(name="owner_id") + ) + @Column(name="val") + @MapKeyColumn(name="param") + public Map getRequestParameters() { + return requestParameters; + } + + /** + * @param params the params to set + */ + public void setRequestParameters(Map params) { + this.requestParameters = params; + } + + /** + * @return the approved + */ + @Basic + @Column(name = "approved") + public boolean isApproved() { + return approved; + } + + /** + * @param approved the approved to set + */ + public void setApproved(boolean approved) { + this.approved = approved; + } + + /** + * The authentication in place when this token was created. + * @return the authentication + */ + @ManyToOne + @JoinColumn(name = "auth_holder_id") + public AuthenticationHolderEntity getAuthenticationHolder() { + return authenticationHolder; + } + + /** + * @param authentication the authentication to set + */ + public void setAuthenticationHolder(AuthenticationHolderEntity authenticationHolder) { + this.authenticationHolder = authenticationHolder; + } + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java index c1cd1be642..d1bda807b7 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java @@ -1,25 +1,25 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; -import java.text.ParseException; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -29,6 +29,7 @@ import javax.persistence.CascadeType; import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -36,19 +37,26 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; -import javax.persistence.OneToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.Transient; +import org.mitre.oauth2.model.convert.JWTStringConverter; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.uma.model.Permission; import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson1Deserializer; +import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson1Serializer; +import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Deserializer; +import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Serializer; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTParser; /** * @author jricher @@ -57,19 +65,39 @@ @Entity @Table(name = "access_token") @NamedQueries({ - @NamedQuery(name = "OAuth2AccessTokenEntity.getAll", query = "select a from OAuth2AccessTokenEntity a"), - @NamedQuery(name = "OAuth2AccessTokenEntity.getAllExpiredByDate", query = "select a from OAuth2AccessTokenEntity a where a.expiration <= :date"), - @NamedQuery(name = "OAuth2AccessTokenEntity.getByRefreshToken", query = "select a from OAuth2AccessTokenEntity a where a.refreshToken = :refreshToken"), - @NamedQuery(name = "OAuth2AccessTokenEntity.getByClient", query = "select a from OAuth2AccessTokenEntity a where a.client = :client"), - @NamedQuery(name = "OAuth2AccessTokenEntity.getByAuthentication", query = "select a from OAuth2AccessTokenEntity a where a.authenticationHolder.authentication = :authentication"), - @NamedQuery(name = "OAuth2AccessTokenEntity.getByIdToken", query = "select a from OAuth2AccessTokenEntity a where a.idToken = :idToken"), - @NamedQuery(name = "OAuth2AccessTokenEntity.getByTokenValue", query = "select a from OAuth2AccessTokenEntity a where a.value = :tokenValue") + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_ALL, query = "select a from OAuth2AccessTokenEntity a"), + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_EXPIRED_BY_DATE, query = "select a from OAuth2AccessTokenEntity a where a.expiration <= :" + OAuth2AccessTokenEntity.PARAM_DATE), + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_REFRESH_TOKEN, query = "select a from OAuth2AccessTokenEntity a where a.refreshToken = :" + OAuth2AccessTokenEntity.PARAM_REFERSH_TOKEN), + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_CLIENT, query = "select a from OAuth2AccessTokenEntity a where a.client = :" + OAuth2AccessTokenEntity.PARAM_CLIENT), + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_TOKEN_VALUE, query = "select a from OAuth2AccessTokenEntity a where a.jwt = :" + OAuth2AccessTokenEntity.PARAM_TOKEN_VALUE), + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_APPROVED_SITE, query = "select a from OAuth2AccessTokenEntity a where a.approvedSite = :" + OAuth2AccessTokenEntity.PARAM_APPROVED_SITE), + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_RESOURCE_SET, query = "select a from OAuth2AccessTokenEntity a join a.permissions p where p.resourceSet.id = :" + OAuth2AccessTokenEntity.PARAM_RESOURCE_SET_ID), + @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_NAME, query = "select r from OAuth2AccessTokenEntity r where r.authenticationHolder.userAuth.name = :" + OAuth2AccessTokenEntity.PARAM_NAME) }) -//@JsonSerialize(using = OAuth2AccessTokenSerializer.class) -//@JsonDeserialize(using = OAuth2AccessTokenDeserializer.class) +@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class) +@org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class) +@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class) +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class) public class OAuth2AccessTokenEntity implements OAuth2AccessToken { - public static String ID_TOKEN_FIELD_NAME = "id_token"; + public static final String QUERY_BY_APPROVED_SITE = "OAuth2AccessTokenEntity.getByApprovedSite"; + public static final String QUERY_BY_TOKEN_VALUE = "OAuth2AccessTokenEntity.getByTokenValue"; + public static final String QUERY_BY_CLIENT = "OAuth2AccessTokenEntity.getByClient"; + public static final String QUERY_BY_REFRESH_TOKEN = "OAuth2AccessTokenEntity.getByRefreshToken"; + public static final String QUERY_EXPIRED_BY_DATE = "OAuth2AccessTokenEntity.getAllExpiredByDate"; + public static final String QUERY_ALL = "OAuth2AccessTokenEntity.getAll"; + public static final String QUERY_BY_RESOURCE_SET = "OAuth2AccessTokenEntity.getByResourceSet"; + public static final String QUERY_BY_NAME = "OAuth2AccessTokenEntity.getByName"; + + public static final String PARAM_TOKEN_VALUE = "tokenValue"; + public static final String PARAM_CLIENT = "client"; + public static final String PARAM_REFERSH_TOKEN = "refreshToken"; + public static final String PARAM_DATE = "date"; + public static final String PARAM_RESOURCE_SET_ID = "rsid"; + public static final String PARAM_APPROVED_SITE = "approvedSite"; + public static final String PARAM_NAME = "name"; + + public static final String ID_TOKEN_FIELD_NAME = "id_token"; private Long id; @@ -79,8 +107,6 @@ public class OAuth2AccessTokenEntity implements OAuth2AccessToken { private JWT jwtValue; // JWT-encoded access token value - private OAuth2AccessTokenEntity idToken; // JWT-encoded OpenID Connect IdToken - private Date expiration; private String tokenType = OAuth2AccessToken.BEARER_TYPE; @@ -89,6 +115,12 @@ public class OAuth2AccessTokenEntity implements OAuth2AccessToken { private Set scope; + private Set permissions; + + private ApprovedSite approvedSite; + + private Map additionalInformation = new HashMap<>(); // ephemeral map of items to be added to the OAuth token response + /** * Create a new, blank access token */ @@ -114,16 +146,13 @@ public void setId(Long id) { } /** - * Get all additional information to be sent to the serializer. Inserts a copy of the IdToken (in JWT String form). + * Get all additional information to be sent to the serializer as part of the token response. + * This map is not persisted to the database. */ @Override @Transient public Map getAdditionalInformation() { - Map map = new HashMap(); //super.getAdditionalInformation(); - if (getIdToken() != null) { - map.put(ID_TOKEN_FIELD_NAME, getIdTokenString()); - } - return map; + return additionalInformation; } /** @@ -163,22 +192,11 @@ public void setClient(ClientDetailsEntity client) { * Get the string-encoded value of this access token. */ @Override - @Basic - @Column(name="token_value") + @Transient public String getValue() { return jwtValue.serialize(); } - /** - * Set the "value" of this Access Token - * - * @param value the JWT string - * @throws ParseException if "value" is not a properly formatted JWT string - */ - public void setValue(String value) throws ParseException { - setJwt(JWTParser.parse(value)); - } - @Override @Basic @Temporal(javax.persistence.TemporalType.TIMESTAMP) @@ -241,38 +259,12 @@ public boolean isExpired() { return getExpiration() == null ? false : System.currentTimeMillis() > getExpiration().getTime(); } - /** - * @return the idToken - */ - @OneToOne(cascade=CascadeType.ALL) // one-to-one mapping for now - @JoinColumn(name = "id_token_id") - public OAuth2AccessTokenEntity getIdToken() { - return idToken; - } - - /** - * @param idToken the idToken to set - */ - public void setIdToken(OAuth2AccessTokenEntity idToken) { - this.idToken = idToken; - } - - /** - * @return the idTokenString - */ - @Transient - public String getIdTokenString() { - if (idToken != null) { - return idToken.getValue(); // get the JWT string value of the id token entity - } else { - return null; - } - } - /** * @return the jwtValue */ - @Transient + @Basic + @Column(name="token_value") + @Convert(converter = JWTStringConverter.class) public JWT getJwt() { return jwtValue; } @@ -300,4 +292,44 @@ public int getExpiresIn() { } } + /** + * @return the permissions + */ + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable( + name = "access_token_permissions", + joinColumns = @JoinColumn(name = "access_token_id"), + inverseJoinColumns = @JoinColumn(name = "permission_id") + ) + public Set getPermissions() { + return permissions; + } + + /** + * @param permissions the permissions to set + */ + public void setPermissions(Set permissions) { + this.permissions = permissions; + } + + @ManyToOne + @JoinColumn(name="approved_site_id") + public ApprovedSite getApprovedSite() { + return approvedSite; + } + + public void setApprovedSite(ApprovedSite approvedSite) { + this.approvedSite = approvedSite; + } + + /** + * Add the ID Token to the additionalInformation map for a token response. + * @param idToken + */ + @Transient + public void setIdToken(JWT idToken) { + if (idToken != null) { + additionalInformation.put(ID_TOKEN_FIELD_NAME, idToken.serialize()); + } + } } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java index 6b061cfd16..f6c2d2153c 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java @@ -1,29 +1,30 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; -import java.text.ParseException; import java.util.Date; import javax.persistence.Basic; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; @@ -37,10 +38,10 @@ import javax.persistence.Temporal; import javax.persistence.Transient; +import org.mitre.oauth2.model.convert.JWTStringConverter; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTParser; /** * @author jricher @@ -49,14 +50,25 @@ @Entity @Table(name = "refresh_token") @NamedQueries({ - @NamedQuery(name = "OAuth2RefreshTokenEntity.getAll", query = "select r from OAuth2RefreshTokenEntity r"), - @NamedQuery(name = "OAuth2RefreshTokenEntity.getAllExpiredByDate", query = "select r from OAuth2RefreshTokenEntity r where r.expiration <= :date"), - @NamedQuery(name = "OAuth2RefreshTokenEntity.getByClient", query = "select r from OAuth2RefreshTokenEntity r where r.client = :client"), - @NamedQuery(name = "OAuth2RefreshTokenEntity.getByTokenValue", query = "select r from OAuth2RefreshTokenEntity r where r.value = :tokenValue"), - @NamedQuery(name = "OAuth2RefreshTokenEntity.getByAuthentication", query = "select r from OAuth2RefreshTokenEntity r where r.authenticationHolder.authentication = :authentication") + @NamedQuery(name = OAuth2RefreshTokenEntity.QUERY_ALL, query = "select r from OAuth2RefreshTokenEntity r"), + @NamedQuery(name = OAuth2RefreshTokenEntity.QUERY_EXPIRED_BY_DATE, query = "select r from OAuth2RefreshTokenEntity r where r.expiration <= :" + OAuth2RefreshTokenEntity.PARAM_DATE), + @NamedQuery(name = OAuth2RefreshTokenEntity.QUERY_BY_CLIENT, query = "select r from OAuth2RefreshTokenEntity r where r.client = :" + OAuth2RefreshTokenEntity.PARAM_CLIENT), + @NamedQuery(name = OAuth2RefreshTokenEntity.QUERY_BY_TOKEN_VALUE, query = "select r from OAuth2RefreshTokenEntity r where r.jwt = :" + OAuth2RefreshTokenEntity.PARAM_TOKEN_VALUE), + @NamedQuery(name = OAuth2RefreshTokenEntity.QUERY_BY_NAME, query = "select r from OAuth2RefreshTokenEntity r where r.authenticationHolder.userAuth.name = :" + OAuth2RefreshTokenEntity.PARAM_NAME) }) public class OAuth2RefreshTokenEntity implements OAuth2RefreshToken { + public static final String QUERY_BY_TOKEN_VALUE = "OAuth2RefreshTokenEntity.getByTokenValue"; + public static final String QUERY_BY_CLIENT = "OAuth2RefreshTokenEntity.getByClient"; + public static final String QUERY_EXPIRED_BY_DATE = "OAuth2RefreshTokenEntity.getAllExpiredByDate"; + public static final String QUERY_ALL = "OAuth2RefreshTokenEntity.getAll"; + public static final String QUERY_BY_NAME = "OAuth2RefreshTokenEntity.getByName"; + + public static final String PARAM_TOKEN_VALUE = "tokenValue"; + public static final String PARAM_CLIENT = "client"; + public static final String PARAM_DATE = "date"; + public static final String PARAM_NAME = "name"; + private Long id; private AuthenticationHolderEntity authenticationHolder; @@ -70,7 +82,7 @@ public class OAuth2RefreshTokenEntity implements OAuth2RefreshToken { private Date expiration; /** - * + * */ public OAuth2RefreshTokenEntity() { @@ -96,7 +108,7 @@ public void setId(Long id) { /** * The authentication in place when the original access token was * created - * + * * @return the authentication */ @ManyToOne @@ -116,21 +128,11 @@ public void setAuthenticationHolder(AuthenticationHolderEntity authenticationHol * Get the JWT-encoded value of this token */ @Override - @Basic - @Column(name="token_value") + @Transient public String getValue() { return jwt.serialize(); } - /** - * Set the value of this token as a string. Parses the string into a JWT. - * @param value - * @throws ParseException if the value is not a valid JWT string - */ - public void setValue(String value) throws ParseException { - setJwt(JWTParser.parse(value)); - } - @Basic @Temporal(javax.persistence.TemporalType.TIMESTAMP) @Column(name = "expiration") @@ -175,7 +177,9 @@ public void setClient(ClientDetailsEntity client) { * Get the JWT object directly * @return the jwt */ - @Transient + @Basic + @Column(name="token_value") + @Convert(converter = JWTStringConverter.class) public JWT getJwt() { return jwt; } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/PKCEAlgorithm.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/PKCEAlgorithm.java new file mode 100644 index 0000000000..5b5d5a5478 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/PKCEAlgorithm.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model; + +import com.nimbusds.jose.Algorithm; +import com.nimbusds.jose.Requirement; + +/** + * @author jricher + * + */ +public final class PKCEAlgorithm extends Algorithm { + + /** + * + */ + private static final long serialVersionUID = 7752852583210088925L; + + public static final PKCEAlgorithm plain = new PKCEAlgorithm("plain", Requirement.REQUIRED); + + public static final PKCEAlgorithm S256 = new PKCEAlgorithm("S256", Requirement.OPTIONAL); + + public PKCEAlgorithm(String name, Requirement req) { + super(name, req); + } + + public PKCEAlgorithm(String name) { + super(name, null); + } + + public static PKCEAlgorithm parse(final String s) { + if (s.equals(plain.getName())) { + return plain; + } else if (s.equals(S256.getName())) { + return S256; + } else { + return new PKCEAlgorithm(s); + } + } + + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java index 50a2cd3d1a..6e4003937f 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; @@ -23,17 +24,17 @@ import java.util.Map; import java.util.Set; -import org.mitre.jose.JWEAlgorithmEmbed; -import org.mitre.jose.JWEEncryptionMethodEmbed; -import org.mitre.jose.JWSAlgorithmEmbed; import org.mitre.oauth2.model.ClientDetailsEntity.AppType; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; import org.springframework.security.core.GrantedAuthority; +import com.google.gson.JsonObject; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWT; /** * @author jricher @@ -47,9 +48,10 @@ public class RegisteredClient { private Date clientSecretExpiresAt; private Date clientIdIssuedAt; private ClientDetailsEntity client; + private JsonObject src; /** - * + * */ public RegisteredClient() { this.client = new ClientDetailsEntity(); @@ -463,6 +465,22 @@ public String getJwksUri() { public void setJwksUri(String jwksUri) { client.setJwksUri(jwksUri); } + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getJwks() + */ + public JWKSet getJwks() { + return client.getJwks(); + } + + /** + * @param jwks + * @see org.mitre.oauth2.model.ClientDetailsEntity#setJwks(com.nimbusds.jose.jwk.JWKSet) + */ + public void setJwks(JWKSet jwks) { + client.setJwks(jwks); + } + /** * @return * @see org.mitre.oauth2.model.ClientDetailsEntity#getSectorIdentifierUri() @@ -549,17 +567,17 @@ public void setInitiateLoginUri(String initiateLoginUri) { } /** * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getPostLogoutRedirectUri() + * @see org.mitre.oauth2.model.ClientDetailsEntity#getPostLogoutRedirectUris() */ - public String getPostLogoutRedirectUri() { - return client.getPostLogoutRedirectUri(); + public Set getPostLogoutRedirectUris() { + return client.getPostLogoutRedirectUris(); } /** * @param postLogoutRedirectUri - * @see org.mitre.oauth2.model.ClientDetailsEntity#setPostLogoutRedirectUri(java.lang.String) + * @see org.mitre.oauth2.model.ClientDetailsEntity#setPostLogoutRedirectUris(java.lang.String) */ - public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { - client.setPostLogoutRedirectUri(postLogoutRedirectUri); + public void setPostLogoutRedirectUris(Set postLogoutRedirectUri) { + client.setPostLogoutRedirectUris(postLogoutRedirectUri); } /** * @return @@ -575,117 +593,6 @@ public Set getRequestUris() { public void setRequestUris(Set requestUris) { client.setRequestUris(requestUris); } - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getRequestObjectSigningAlgEmbed() - */ - public JWSAlgorithmEmbed getRequestObjectSigningAlgEmbed() { - return client.getRequestObjectSigningAlgEmbed(); - } - - /** - * @param requestObjectSigningAlg - * @see org.mitre.oauth2.model.ClientDetailsEntity#setRequestObjectSigningAlgEmbed(org.mitre.jose.JWSAlgorithmEmbed) - */ - public void setRequestObjectSigningAlgEmbed(JWSAlgorithmEmbed requestObjectSigningAlg) { - client.setRequestObjectSigningAlgEmbed(requestObjectSigningAlg); - } - - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getUserInfoSignedResponseAlgEmbed() - */ - public JWSAlgorithmEmbed getUserInfoSignedResponseAlgEmbed() { - return client.getUserInfoSignedResponseAlgEmbed(); - } - - /** - * @param userInfoSignedResponseAlg - * @see org.mitre.oauth2.model.ClientDetailsEntity#setUserInfoSignedResponseAlgEmbed(org.mitre.jose.JWSAlgorithmEmbed) - */ - public void setUserInfoSignedResponseAlgEmbed(JWSAlgorithmEmbed userInfoSignedResponseAlg) { - client.setUserInfoSignedResponseAlgEmbed(userInfoSignedResponseAlg); - } - - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getUserInfoEncryptedResponseAlgEmbed() - */ - public JWEAlgorithmEmbed getUserInfoEncryptedResponseAlgEmbed() { - return client.getUserInfoEncryptedResponseAlgEmbed(); - } - - /** - * @param userInfoEncryptedResponseAlg - * @see org.mitre.oauth2.model.ClientDetailsEntity#setUserInfoEncryptedResponseAlgEmbed(org.mitre.jose.JWEAlgorithmEmbed) - */ - public void setUserInfoEncryptedResponseAlgEmbed(JWEAlgorithmEmbed userInfoEncryptedResponseAlg) { - client.setUserInfoEncryptedResponseAlgEmbed(userInfoEncryptedResponseAlg); - } - - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getUserInfoEncryptedResponseEncEmbed() - */ - public JWEEncryptionMethodEmbed getUserInfoEncryptedResponseEncEmbed() { - return client.getUserInfoEncryptedResponseEncEmbed(); - } - - /** - * @param userInfoEncryptedResponseEnc - * @see org.mitre.oauth2.model.ClientDetailsEntity#setUserInfoEncryptedResponseEncEmbed(org.mitre.jose.JWEEncryptionMethodEmbed) - */ - public void setUserInfoEncryptedResponseEncEmbed(JWEEncryptionMethodEmbed userInfoEncryptedResponseEnc) { - client.setUserInfoEncryptedResponseEncEmbed(userInfoEncryptedResponseEnc); - } - - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getIdTokenSignedResponseAlgEmbed() - */ - public JWSAlgorithmEmbed getIdTokenSignedResponseAlgEmbed() { - return client.getIdTokenSignedResponseAlgEmbed(); - } - - /** - * @param idTokenSignedResponseAlg - * @see org.mitre.oauth2.model.ClientDetailsEntity#setIdTokenSignedResponseAlgEmbed(org.mitre.jose.JWSAlgorithmEmbed) - */ - public void setIdTokenSignedResponseAlgEmbed(JWSAlgorithmEmbed idTokenSignedResponseAlg) { - client.setIdTokenSignedResponseAlgEmbed(idTokenSignedResponseAlg); - } - - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getIdTokenEncryptedResponseAlgEmbed() - */ - public JWEAlgorithmEmbed getIdTokenEncryptedResponseAlgEmbed() { - return client.getIdTokenEncryptedResponseAlgEmbed(); - } - - /** - * @param idTokenEncryptedResponseAlg - * @see org.mitre.oauth2.model.ClientDetailsEntity#setIdTokenEncryptedResponseAlgEmbed(org.mitre.jose.JWEAlgorithmEmbed) - */ - public void setIdTokenEncryptedResponseAlgEmbed(JWEAlgorithmEmbed idTokenEncryptedResponseAlg) { - client.setIdTokenEncryptedResponseAlgEmbed(idTokenEncryptedResponseAlg); - } - - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getIdTokenEncryptedResponseEncEmbed() - */ - public JWEEncryptionMethodEmbed getIdTokenEncryptedResponseEncEmbed() { - return client.getIdTokenEncryptedResponseEncEmbed(); - } - - /** - * @param idTokenEncryptedResponseEnc - * @see org.mitre.oauth2.model.ClientDetailsEntity#setIdTokenEncryptedResponseEncEmbed(org.mitre.jose.JWEEncryptionMethodEmbed) - */ - public void setIdTokenEncryptedResponseEncEmbed(JWEEncryptionMethodEmbed idTokenEncryptedResponseEnc) { - client.setIdTokenEncryptedResponseEncEmbed(idTokenEncryptedResponseEnc); - } /** * @return @@ -799,22 +706,6 @@ public void setIdTokenEncryptedResponseEnc(EncryptionMethod idTokenEncryptedResp client.setIdTokenEncryptedResponseEnc(idTokenEncryptedResponseEnc); } - /** - * @return - * @see org.mitre.oauth2.model.ClientDetailsEntity#getTokenEndpointAuthSigningAlgEmbed() - */ - public JWSAlgorithmEmbed getTokenEndpointAuthSigningAlgEmbed() { - return client.getTokenEndpointAuthSigningAlgEmbed(); - } - - /** - * @param tokenEndpointAuthSigningAlgEmbed - * @see org.mitre.oauth2.model.ClientDetailsEntity#setTokenEndpointAuthSigningAlgEmbed(org.mitre.jose.JWSAlgorithmEmbed) - */ - public void setTokenEndpointAuthSigningAlgEmbed(JWSAlgorithmEmbed tokenEndpointAuthSigningAlgEmbed) { - client.setTokenEndpointAuthSigningAlgEmbed(tokenEndpointAuthSigningAlgEmbed); - } - /** * @return * @see org.mitre.oauth2.model.ClientDetailsEntity#getTokenEndpointAuthSigningAlg() @@ -894,6 +785,116 @@ public void setClientIdIssuedAt(Date issuedAt) { this.clientIdIssuedAt = issuedAt; } + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getClaimsRedirectUris() + */ + public Set getClaimsRedirectUris() { + return client.getClaimsRedirectUris(); + } + + /** + * @param claimsRedirectUris + * @see org.mitre.oauth2.model.ClientDetailsEntity#setClaimsRedirectUris(java.util.Set) + */ + public void setClaimsRedirectUris(Set claimsRedirectUris) { + client.setClaimsRedirectUris(claimsRedirectUris); + } + + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getSoftwareStatement() + */ + public JWT getSoftwareStatement() { + return client.getSoftwareStatement(); + } + + /** + * @param softwareStatement + * @see org.mitre.oauth2.model.ClientDetailsEntity#setSoftwareStatement(com.nimbusds.jwt.JWT) + */ + public void setSoftwareStatement(JWT softwareStatement) { + client.setSoftwareStatement(softwareStatement); + } + + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getCodeChallengeMethod() + */ + public PKCEAlgorithm getCodeChallengeMethod() { + return client.getCodeChallengeMethod(); + } + + /** + * @param codeChallengeMethod + * @see org.mitre.oauth2.model.ClientDetailsEntity#setCodeChallengeMethod(org.mitre.oauth2.model.PKCEAlgorithm) + */ + public void setCodeChallengeMethod(PKCEAlgorithm codeChallengeMethod) { + client.setCodeChallengeMethod(codeChallengeMethod); + } + + /** + * @return the src + */ + public JsonObject getSource() { + return src; + } + + /** + * @param src the src to set + */ + public void setSource(JsonObject src) { + this.src = src; + } + + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getDeviceCodeValiditySeconds() + */ + public Integer getDeviceCodeValiditySeconds() { + return client.getDeviceCodeValiditySeconds(); + } + + /** + * @param deviceCodeValiditySeconds + * @see org.mitre.oauth2.model.ClientDetailsEntity#setDeviceCodeValiditySeconds(java.lang.Integer) + */ + public void setDeviceCodeValiditySeconds(Integer deviceCodeValiditySeconds) { + client.setDeviceCodeValiditySeconds(deviceCodeValiditySeconds); + } + + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getSoftwareId() + */ + public String getSoftwareId() { + return client.getSoftwareId(); + } + + /** + * @param softwareId + * @see org.mitre.oauth2.model.ClientDetailsEntity#setSoftwareId(java.lang.String) + */ + public void setSoftwareId(String softwareId) { + client.setSoftwareId(softwareId); + } + + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getSoftwareVersion() + */ + public String getSoftwareVersion() { + return client.getSoftwareVersion(); + } + + /** + * @param softwareVersion + * @see org.mitre.oauth2.model.ClientDetailsEntity#setSoftwareVersion(java.lang.String) + */ + public void setSoftwareVersion(String softwareVersion) { + client.setSoftwareVersion(softwareVersion); + } + } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java new file mode 100644 index 0000000000..79231b5238 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model; + +public interface RegisteredClientFields { + public String SOFTWARE_ID = "software_id"; + public String SOFTWARE_VERSION = "software_version"; + public String SOFTWARE_STATEMENT = "software_statement"; + public String CLAIMS_REDIRECT_URIS = "claims_redirect_uris"; + public String CLIENT_SECRET_EXPIRES_AT = "client_secret_expires_at"; + public String CLIENT_ID_ISSUED_AT = "client_id_issued_at"; + public String REGISTRATION_CLIENT_URI = "registration_client_uri"; + public String REGISTRATION_ACCESS_TOKEN = "registration_access_token"; + public String REQUEST_URIS = "request_uris"; + public String POST_LOGOUT_REDIRECT_URIS = "post_logout_redirect_uris"; + public String INITIATE_LOGIN_URI = "initiate_login_uri"; + public String DEFAULT_ACR_VALUES = "default_acr_values"; + public String REQUIRE_AUTH_TIME = "require_auth_time"; + public String DEFAULT_MAX_AGE = "default_max_age"; + public String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg"; + public String ID_TOKEN_ENCRYPTED_RESPONSE_ENC = "id_token_encrypted_response_enc"; + public String ID_TOKEN_ENCRYPTED_RESPONSE_ALG = "id_token_encrypted_response_alg"; + public String ID_TOKEN_SIGNED_RESPONSE_ALG = "id_token_signed_response_alg"; + public String USERINFO_ENCRYPTED_RESPONSE_ENC = "userinfo_encrypted_response_enc"; + public String USERINFO_ENCRYPTED_RESPONSE_ALG = "userinfo_encrypted_response_alg"; + public String USERINFO_SIGNED_RESPONSE_ALG = "userinfo_signed_response_alg"; + public String REQUEST_OBJECT_SIGNING_ALG = "request_object_signing_alg"; + public String SUBJECT_TYPE = "subject_type"; + public String SECTOR_IDENTIFIER_URI = "sector_identifier_uri"; + public String APPLICATION_TYPE = "application_type"; + public String JWKS_URI = "jwks_uri"; + public String JWKS = "jwks"; + public String SCOPE_SEPARATOR = " "; + public String POLICY_URI = "policy_uri"; + public String RESPONSE_TYPES = "response_types"; + public String GRANT_TYPES = "grant_types"; + public String SCOPE = "scope"; + public String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method"; + public String TOS_URI = "tos_uri"; + public String CONTACTS = "contacts"; + public String LOGO_URI = "logo_uri"; + public String CLIENT_URI = "client_uri"; + public String CLIENT_NAME = "client_name"; + public String REDIRECT_URIS = "redirect_uris"; + public String CLIENT_SECRET = "client_secret"; + public String CLIENT_ID = "client_id"; + public String CODE_CHALLENGE_METHOD = "code_challenge_method"; +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/SavedUserAuthentication.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/SavedUserAuthentication.java new file mode 100644 index 0000000000..21fa34a830 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/SavedUserAuthentication.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model; + +import java.util.Collection; +import java.util.HashSet; + +import javax.persistence.Basic; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.mitre.oauth2.model.convert.SimpleGrantedAuthorityStringConverter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +/** + * This class stands in for an original Authentication object. + * + * @author jricher + * + */ +@Entity +@Table(name="saved_user_auth") +public class SavedUserAuthentication implements Authentication { + + private static final long serialVersionUID = -1804249963940323488L; + + private Long id; + + private String name; + + private Collection authorities; + + private boolean authenticated; + + private String sourceClass; + + /** + * Create a Saved Auth from an existing Auth token + */ + public SavedUserAuthentication(Authentication src) { + setName(src.getName()); + setAuthorities(new HashSet<>(src.getAuthorities())); + setAuthenticated(src.isAuthenticated()); + + if (src instanceof SavedUserAuthentication) { + // if we're copying in a saved auth, carry over the original class name + setSourceClass(((SavedUserAuthentication) src).getSourceClass()); + } else { + setSourceClass(src.getClass().getName()); + } + } + + /** + * Create an empty saved auth + */ + public SavedUserAuthentication() { + + } + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + @Override + @Basic + @Column(name="name") + public String getName() { + return name; + } + + @Override + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="saved_user_auth_authority", + joinColumns=@JoinColumn(name="owner_id") + ) + @Convert(converter = SimpleGrantedAuthorityStringConverter.class) + @Column(name="authority") + public Collection getAuthorities() { + return authorities; + } + + @Override + @Transient + public Object getCredentials() { + return ""; + } + + @Override + @Transient + public Object getDetails() { + return null; + } + + @Override + @Transient + public Object getPrincipal() { + return getName(); + } + + @Override + @Basic + @Column(name="authenticated") + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + this.authenticated = isAuthenticated; + } + + /** + * @return the sourceClass + */ + @Basic + @Column(name="source_class") + public String getSourceClass() { + return sourceClass; + } + + /** + * @param sourceClass the sourceClass to set + */ + public void setSourceClass(String sourceClass) { + this.sourceClass = sourceClass; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @param authorities the authorities to set + */ + public void setAuthorities(Collection authorities) { + this.authorities = authorities; + } + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/SystemScope.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/SystemScope.java index ff30857a31..0807b160e8 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/SystemScope.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/SystemScope.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; @@ -28,7 +29,6 @@ import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; -import javax.persistence.Transient; /** * @author jricher @@ -37,20 +37,22 @@ @Entity @Table(name = "system_scope") @NamedQueries({ - @NamedQuery(name = "SystemScope.findAll", query = "select s from SystemScope s ORDER BY s.id"), - @NamedQuery(name = "SystemScope.getByValue", query = "select s from SystemScope s WHERE s.value = :value") + @NamedQuery(name = SystemScope.QUERY_ALL, query = "select s from SystemScope s ORDER BY s.id"), + @NamedQuery(name = SystemScope.QUERY_BY_VALUE, query = "select s from SystemScope s WHERE s.value = :" + SystemScope.PARAM_VALUE) }) public class SystemScope { + public static final String QUERY_BY_VALUE = "SystemScope.getByValue"; + public static final String QUERY_ALL = "SystemScope.findAll"; + + public static final String PARAM_VALUE = "value"; + private Long id; private String value; // scope value private String description; // human-readable description private String icon; // class of the icon to display on the auth page - private boolean allowDynReg = false; // can a dynamically registered client ask for this scope? private boolean defaultScope = false; // is this a default scope for newly-registered clients? - private boolean structured = false; // is this a default scope for newly-registered clients? - private String structuredParamDescription; - private String structuredValue; + private boolean restricted = false; // is this scope restricted to admin-only registration access? /** * Make a blank system scope with no value @@ -124,20 +126,6 @@ public String getIcon() { public void setIcon(String icon) { this.icon = icon; } - /** - * @return the allowDynReg - */ - @Basic - @Column(name = "allow_dyn_reg") - public boolean isAllowDynReg() { - return allowDynReg; - } - /** - * @param allowDynReg the allowDynReg to set - */ - public void setAllowDynReg(boolean allowDynReg) { - this.allowDynReg = allowDynReg; - } /** * @return the defaultScope @@ -156,51 +144,21 @@ public void setDefaultScope(boolean defaultScope) { } /** - * @return the isStructured status - */ - @Basic - @Column(name = "structured") - public boolean isStructured() { - return structured; - } - - /** - * @param structured the structured to set + * @return the restricted */ - public void setStructured(boolean structured) { - this.structured = structured; - } - @Basic - @Column(name = "structured_param_description") - public String getStructuredParamDescription() { - return structuredParamDescription; + @Column(name = "restricted") + public boolean isRestricted() { + return restricted; } /** - * @param isStructured the isStructured to set + * @param restricted the restricted to set */ - public void setStructuredParamDescription(String d) { - this.structuredParamDescription = d; + public void setRestricted(boolean restricted) { + this.restricted = restricted; } - - /** - * @return the structuredValue - */ - @Transient // we don't save the value of a system scope separately - public String getStructuredValue() { - return structuredValue; - } - - /** - * @param structuredValue the structuredValue to set - */ - public void setStructuredValue(String structuredValue) { - this.structuredValue = structuredValue; - } - - /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @@ -208,14 +166,12 @@ public void setStructuredValue(String structuredValue) { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + (allowDynReg ? 1231 : 1237); result = prime * result + (defaultScope ? 1231 : 1237); - result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + + ((description == null) ? 0 : description.hashCode()); result = prime * result + ((icon == null) ? 0 : icon.hashCode()); result = prime * result + ((id == null) ? 0 : id.hashCode()); - result = prime * result + (structured ? 1231 : 1237); - result = prime * result + ((structuredParamDescription == null) ? 0 : structuredParamDescription.hashCode()); - result = prime * result + ((structuredValue == null) ? 0 : structuredValue.hashCode()); + result = prime * result + (restricted ? 1231 : 1237); result = prime * result + ((value == null) ? 0 : value.hashCode()); return result; } @@ -231,13 +187,10 @@ public boolean equals(Object obj) { if (obj == null) { return false; } - if (!(obj instanceof SystemScope)) { + if (getClass() != obj.getClass()) { return false; } SystemScope other = (SystemScope) obj; - if (allowDynReg != other.allowDynReg) { - return false; - } if (defaultScope != other.defaultScope) { return false; } @@ -262,21 +215,7 @@ public boolean equals(Object obj) { } else if (!id.equals(other.id)) { return false; } - if (structured != other.structured) { - return false; - } - if (structuredParamDescription == null) { - if (other.structuredParamDescription != null) { - return false; - } - } else if (!structuredParamDescription.equals(other.structuredParamDescription)) { - return false; - } - if (structuredValue == null) { - if (other.structuredValue != null) { - return false; - } - } else if (!structuredValue.equals(other.structuredValue)) { + if (restricted != other.restricted) { return false; } if (value == null) { @@ -294,8 +233,9 @@ public boolean equals(Object obj) { */ @Override public String toString() { - return "SystemScope [id=" + id + ", value=" + value + ", description=" + description + ", icon=" + icon + ", allowDynReg=" + allowDynReg + ", defaultScope=" + defaultScope + ", structured=" + structured + ", structuredParamDescription=" + structuredParamDescription + ", structuredValue=" - + structuredValue + "]"; + return "SystemScope [id=" + id + ", value=" + value + ", description=" + + description + ", icon=" + icon + ", defaultScope=" + + defaultScope + ", restricted=" + restricted + "]"; } } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWEAlgorithmStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWEAlgorithmStringConverter.java new file mode 100644 index 0000000000..1341cb4bc8 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWEAlgorithmStringConverter.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import com.nimbusds.jose.JWEAlgorithm; + +@Converter +public class JWEAlgorithmStringConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(JWEAlgorithm attribute) { + if (attribute != null) { + return attribute.getName(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public JWEAlgorithm convertToEntityAttribute(String dbData) { + if (dbData != null) { + return JWEAlgorithm.parse(dbData); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWEEncryptionMethodStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWEEncryptionMethodStringConverter.java new file mode 100644 index 0000000000..a9f0355b8b --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWEEncryptionMethodStringConverter.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import com.nimbusds.jose.EncryptionMethod; + +@Converter +public class JWEEncryptionMethodStringConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(EncryptionMethod attribute) { + if (attribute != null) { + return attribute.getName(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public EncryptionMethod convertToEntityAttribute(String dbData) { + if (dbData != null) { + return EncryptionMethod.parse(dbData); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWKSetStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWKSetStringConverter.java new file mode 100644 index 0000000000..f499e1af4b --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWKSetStringConverter.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import java.text.ParseException; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.nimbusds.jose.jwk.JWKSet; + +/** + * @author jricher + * + */ +@Converter +public class JWKSetStringConverter implements AttributeConverter { + + private static Logger logger = LoggerFactory.getLogger(JWKSetStringConverter.class); + + @Override + public String convertToDatabaseColumn(JWKSet attribute) { + if (attribute != null) { + return attribute.toString(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public JWKSet convertToEntityAttribute(String dbData) { + if (dbData != null) { + try { + JWKSet jwks = JWKSet.parse(dbData); + return jwks; + } catch (ParseException e) { + logger.error("Unable to parse JWK Set", e); + return null; + } + } else { + return null; + } + + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWSAlgorithmStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWSAlgorithmStringConverter.java new file mode 100644 index 0000000000..c671c50fa0 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWSAlgorithmStringConverter.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import com.nimbusds.jose.JWSAlgorithm; + +@Converter +public class JWSAlgorithmStringConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(JWSAlgorithm attribute) { + if (attribute != null) { + return attribute.getName(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public JWSAlgorithm convertToEntityAttribute(String dbData) { + if (dbData != null) { + return JWSAlgorithm.parse(dbData); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWTStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWTStringConverter.java new file mode 100644 index 0000000000..6f69c6a88b --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JWTStringConverter.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import java.text.ParseException; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + +/** + * @author jricher + * + */ +@Converter +public class JWTStringConverter implements AttributeConverter { + + public static Logger logger = LoggerFactory.getLogger(JWTStringConverter.class); + + @Override + public String convertToDatabaseColumn(JWT attribute) { + if (attribute != null) { + return attribute.serialize(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public JWT convertToEntityAttribute(String dbData) { + if (dbData != null) { + try { + JWT jwt = JWTParser.parse(dbData); + return jwt; + } catch (ParseException e) { + logger.error("Unable to parse JWT", e); + return null; + } + } else { + return null; + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JsonElementStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JsonElementStringConverter.java new file mode 100644 index 0000000000..3ee6305372 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/JsonElementStringConverter.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import com.google.common.base.Strings; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +/** + * @author jricher + * + */ +@Converter +public class JsonElementStringConverter implements AttributeConverter { + + private JsonParser parser = new JsonParser(); + + @Override + public String convertToDatabaseColumn(JsonElement attribute) { + if (attribute != null) { + return attribute.toString(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public JsonElement convertToEntityAttribute(String dbData) { + if (!Strings.isNullOrEmpty(dbData)) { + return parser.parse(dbData); + } else { + return null; + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/PKCEAlgorithmStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/PKCEAlgorithmStringConverter.java new file mode 100644 index 0000000000..4e8359f841 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/PKCEAlgorithmStringConverter.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import org.mitre.oauth2.model.PKCEAlgorithm; + +/** + * @author jricher + * + */ +@Converter +public class PKCEAlgorithmStringConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(PKCEAlgorithm attribute) { + if (attribute != null) { + return attribute.getName(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public PKCEAlgorithm convertToEntityAttribute(String dbData) { + if (dbData != null) { + return PKCEAlgorithm.parse(dbData); + } else { + return null; + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/SerializableStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/SerializableStringConverter.java new file mode 100644 index 0000000000..0c3e523884 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/SerializableStringConverter.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import java.io.Serializable; +import java.util.Date; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Translates a Serializable object of certain primitive types + * into a String for storage in the database, for use with the + * OAuth2Request extensions map. + * + * This class does allow some extension data to be lost. + * + * @author jricher + * + */ +@Converter +public class SerializableStringConverter implements AttributeConverter { + + private static Logger logger = LoggerFactory.getLogger(SerializableStringConverter.class); + + @Override + public String convertToDatabaseColumn(Serializable attribute) { + if (attribute == null) { + return null; + } else if (attribute instanceof String) { + return (String) attribute; + } else if (attribute instanceof Long) { + return attribute.toString(); + } else if (attribute instanceof Date) { + return Long.toString(((Date)attribute).getTime()); + } else { + logger.warn("Dropping data from request: " + attribute + " :: " + attribute.getClass()); + return null; + } + } + + @Override + public Serializable convertToEntityAttribute(String dbData) { + return dbData; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/SimpleGrantedAuthorityStringConverter.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/SimpleGrantedAuthorityStringConverter.java new file mode 100644 index 0000000000..875387508f --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/convert/SimpleGrantedAuthorityStringConverter.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +/** + * @author jricher + * + */ +@Converter +public class SimpleGrantedAuthorityStringConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(SimpleGrantedAuthority attribute) { + if (attribute != null) { + return attribute.getAuthority(); + } else { + return null; + } + } + + @Override + public SimpleGrantedAuthority convertToEntityAttribute(String dbData) { + if (dbData != null) { + return new SimpleGrantedAuthority(dbData); + } else { + return null; + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthenticationHolderRepository.java b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthenticationHolderRepository.java index 9c081a58c9..1b217de3e2 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthenticationHolderRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthenticationHolderRepository.java @@ -1,39 +1,37 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository; import java.util.List; +import org.mitre.data.PageCriteria; import org.mitre.oauth2.model.AuthenticationHolderEntity; -import org.springframework.security.oauth2.provider.OAuth2Authentication; public interface AuthenticationHolderRepository { + public List getAll(); public AuthenticationHolderEntity getById(Long id); - public AuthenticationHolderEntity getByAuthentication(OAuth2Authentication a); - - public void removeById(Long id); - public void remove(AuthenticationHolderEntity a); public AuthenticationHolderEntity save(AuthenticationHolderEntity a); public List getOrphanedAuthenticationHolders(); - + public List getOrphanedAuthenticationHolders(PageCriteria pageCriteria); } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthorizationCodeRepository.java b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthorizationCodeRepository.java index c35bc9edb9..11375e7e64 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthorizationCodeRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/AuthorizationCodeRepository.java @@ -1,28 +1,30 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository; +import java.util.Collection; + +import org.mitre.data.PageCriteria; import org.mitre.oauth2.model.AuthorizationCodeEntity; -import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; -import org.springframework.security.oauth2.provider.OAuth2Authentication; /** * Interface for saving and consuming OAuth2 authorization codes as AuthorizationCodeEntitys. - * + * * @author aanganes * */ @@ -30,19 +32,36 @@ public interface AuthorizationCodeRepository { /** * Save an AuthorizationCodeEntity to the repository - * + * * @param authorizationCode the AuthorizationCodeEntity to save * @return the saved AuthorizationCodeEntity */ public AuthorizationCodeEntity save(AuthorizationCodeEntity authorizationCode); /** - * Consume an authorization code. - * + * Get an authorization code from the repository by value. + * * @param code the authorization code value * @return the authentication associated with the code - * @throws InvalidGrantException if no AuthorizationCodeEntity is found with the given value */ - public OAuth2Authentication consume(String code) throws InvalidGrantException; + public AuthorizationCodeEntity getByCode(String code); + + /** + * Remove an authorization code from the repository + * + * @param authorizationCodeEntity + */ + public void remove(AuthorizationCodeEntity authorizationCodeEntity); + + /** + * @return A collection of all expired codes. + */ + public Collection getExpiredCodes(); + + /** + * @return A collection of all expired codes, limited by the given + * PageCriteria. + */ + public Collection getExpiredCodes(PageCriteria pageCriteria); } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java index e5ae81a740..56936ac80a 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository; import java.util.Collection; diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java index a81cccac9e..e71d0a5975 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java @@ -1,28 +1,31 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository; import java.util.List; import java.util.Set; +import org.mitre.data.PageCriteria; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; -import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.uma.model.ResourceSet; public interface OAuth2TokenRepository { @@ -49,10 +52,10 @@ public interface OAuth2TokenRepository { public List getAccessTokensForClient(ClientDetailsEntity client); public List getRefreshTokensForClient(ClientDetailsEntity client); - - public OAuth2AccessTokenEntity getByAuthentication(OAuth2Authentication auth); - - public OAuth2AccessTokenEntity getAccessTokenForIdToken(OAuth2AccessTokenEntity idToken); + + public Set getAccessTokensByUserName(String name); + + public Set getRefreshTokensByUserName(String name); public Set getAllAccessTokens(); @@ -60,6 +63,38 @@ public interface OAuth2TokenRepository { public Set getAllExpiredAccessTokens(); + public Set getAllExpiredAccessTokens(PageCriteria pageCriteria); + public Set getAllExpiredRefreshTokens(); + public Set getAllExpiredRefreshTokens(PageCriteria pageCriteria); + + public Set getAccessTokensForResourceSet(ResourceSet rs); + + /** + * removes duplicate access tokens. + * + * @deprecated this method was added to return the remove duplicate access tokens values + * so that {code removeAccessToken(OAuth2AccessTokenEntity o)} would not to fail. the + * removeAccessToken method has been updated so as it will not fail in the event that an + * accessToken has been duplicated, so this method is unnecessary. + * + */ + @Deprecated + public void clearDuplicateAccessTokens(); + + /** + * removes duplicate refresh tokens. + * + * @deprecated this method was added to return the remove duplicate refresh token value + * so that {code removeRefreshToken(OAuth2RefreshTokenEntity o)} would not to fail. the + * removeRefreshToken method has been updated so as it will not fail in the event that + * refreshToken has been duplicated, so this method is unnecessary. + * + */ + @Deprecated + public void clearDuplicateRefreshTokens(); + + public List getAccessTokensForApprovedSite(ApprovedSite approvedSite); + } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/SystemScopeRepository.java b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/SystemScopeRepository.java index 1ccdc49645..8c891d566d 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/SystemScopeRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/SystemScopeRepository.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository; diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/repository/impl/DeviceCodeRepository.java b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/impl/DeviceCodeRepository.java new file mode 100644 index 0000000000..392932642c --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/repository/impl/DeviceCodeRepository.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.repository.impl; + +import java.util.Collection; + +import org.mitre.oauth2.model.DeviceCode; + +/** + * @author jricher + * + */ +public interface DeviceCodeRepository { + + /** + * @param id + * @return + */ + public DeviceCode getById(Long id); + + /** + * @param deviceCode + * @return + */ + public DeviceCode getByDeviceCode(String deviceCode); + + /** + * @param scope + */ + public void remove(DeviceCode scope); + + /** + * @param scope + * @return + */ + public DeviceCode save(DeviceCode scope); + + /** + * @param userCode + * @return + */ + public DeviceCode getByUserCode(String userCode); + + /** + * @return + */ + public Collection getExpiredCodes(); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java index 691c37bb75..08695c6751 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service; import java.util.Collection; diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/DeviceCodeService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/DeviceCodeService.java new file mode 100644 index 0000000000..b9601292ee --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/DeviceCodeService.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service; + +import java.util.Map; +import java.util.Set; + +import org.mitre.oauth2.exception.DeviceCodeCreationException; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.DeviceCode; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.OAuth2Authentication; + +/** + * @author jricher + * + */ +public interface DeviceCodeService { + + /** + * @param userCode + * @return + */ + public DeviceCode lookUpByUserCode(String userCode); + + /** + * @param dc + * @param o2Auth + */ + public DeviceCode approveDeviceCode(DeviceCode dc, OAuth2Authentication o2Auth); + + /** + * @param deviceCode + * @param client + * @return + */ + public DeviceCode findDeviceCode(String deviceCode, ClientDetails client); + + + /** + * + * @param deviceCode + * @param client + */ + public void clearDeviceCode(String deviceCode, ClientDetails client); + + /** + * @param deviceCode + * @param userCode + * @param requestedScopes + * @param client + * @param parameters + * @return + */ + public DeviceCode createNewDeviceCode(Set requestedScopes, ClientDetailsEntity client, Map parameters) throws DeviceCodeCreationException; + + + public void clearExpiredDeviceCodes(); +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionResultAssembler.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionResultAssembler.java new file mode 100644 index 0000000000..e0250a5035 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionResultAssembler.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service; + +import java.text.SimpleDateFormat; +import java.util.Map; +import java.util.Set; + +import javax.swing.text.DateFormatter; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.openid.connect.model.UserInfo; + +/** + * Strategy interface for assembling a token introspection result. + */ +public interface IntrospectionResultAssembler { + + public String TOKEN_TYPE = "token_type"; + public String CLIENT_ID = "client_id"; + public String USER_ID = "user_id"; + public String SUB = "sub"; + public String EXP = "exp"; + public String EXPIRES_AT = "expires_at"; + public String SCOPE_SEPARATOR = " "; + public String SCOPE = "scope"; + public String ACTIVE = "active"; + public DateFormatter dateFormat = new DateFormatter(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")); + + /** + * Assemble a token introspection result from the given access token and user info. + * + * @param accessToken the access token + * @param userInfo the user info + * @param authScopes the scopes the client is authorized for + * @return the token introspection result + */ + Map assembleFrom(OAuth2AccessTokenEntity accessToken, UserInfo userInfo, Set authScopes); + + /** + * Assemble a token introspection result from the given refresh token and user info. + * + * @param refreshToken the refresh token + * @param userInfo the user info + * @param authScopes the scopes the client is authorized for + * @return the token introspection result + */ + Map assembleFrom(OAuth2RefreshTokenEntity refreshToken, UserInfo userInfo, Set authScopes); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java index d9b5b4d6e8..c39ccd90da 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service; import java.util.List; @@ -50,12 +51,6 @@ public interface OAuth2TokenEntityService extends AuthorizationServerTokenServic @Override public OAuth2AccessTokenEntity getAccessToken(OAuth2Authentication authentication); - /** - * @param incomingToken - * @return - */ - public OAuth2AccessTokenEntity getAccessTokenForIdToken(OAuth2AccessTokenEntity idToken); - public OAuth2AccessTokenEntity getAccessTokenById(Long id); public OAuth2RefreshTokenEntity getRefreshTokenById(Long id); @@ -63,4 +58,6 @@ public interface OAuth2TokenEntityService extends AuthorizationServerTokenServic public Set getAllAccessTokensForUser(String name); public Set getAllRefreshTokensForUser(String name); + + public OAuth2AccessTokenEntity getRegistrationAccessTokenForClient(ClientDetailsEntity client); } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/SystemScopeService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/SystemScopeService.java index 0505b2259a..dad93f1711 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/service/SystemScopeService.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/SystemScopeService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service; @@ -23,6 +24,8 @@ import org.mitre.oauth2.model.SystemScope; +import com.google.common.collect.Sets; + /** * @author jricher * @@ -30,9 +33,17 @@ public interface SystemScopeService { public static final String OFFLINE_ACCESS = "offline_access"; - public static final String ID_TOKEN_SCOPE = "id-token"; - public static final String REGISTRATION_TOKEN_SCOPE = "registration-token"; - public static final String RESOURCE_TOKEN_SCOPE = "resource-token"; + public static final String OPENID_SCOPE = "openid"; + public static final String REGISTRATION_TOKEN_SCOPE = "registration-token"; // this scope manages dynamic client registrations + public static final String RESOURCE_TOKEN_SCOPE = "resource-token"; // this scope manages client-style protected resources + public static final String UMA_PROTECTION_SCOPE = "uma_protection"; + public static final String UMA_AUTHORIZATION_SCOPE = "uma_authorization"; + + public static final Set reservedScopes = + Sets.newHashSet( + new SystemScope(REGISTRATION_TOKEN_SCOPE), + new SystemScope(RESOURCE_TOKEN_SCOPE) + ); public Set getAll(); @@ -43,10 +54,25 @@ public interface SystemScopeService { public Set getDefaults(); /** - * Get all scopes that are allowed for dynamic registration on this system + * Get all the reserved system scopes. These can't be used + * by clients directly, but are instead tied to special system + * tokens like id tokens and registration access tokens. + * + * @return + */ + public Set getReserved(); + + /** + * Get all the registered scopes that are restricted. * @return */ - public Set getDynReg(); + public Set getRestricted(); + + /** + * Get all the registered scopes that aren't restricted. + * @return + */ + public Set getUnrestricted(); public SystemScope getById(Long id); @@ -71,19 +97,23 @@ public interface SystemScopeService { public Set toStrings(Set scope); /** - * Test whether the scopes in both sets are compatible, with special - * processing for structured scopes. All scopes in "actual" must exist in - * "expected". If a scope in "expected" is structured and has a value, it - * must be matched exactly by its corresponding scope in "actual". If a - * scope in "expected" is structured but has no value, it may be matched by - * a scope with or without a value in "actual". + * Test whether the scopes in both sets are compatible. All scopes in "actual" must exist in "expected". */ public boolean scopesMatch(Set expected, Set actual); /** - * Remove any system-restricted scopes from the set and return the result. + * Remove any system-reserved or registered restricted scopes from the + * set and return the result. + * @param scopes + * @return + */ + public Set removeRestrictedAndReservedScopes(Set scopes); + + /** + * Remove any system-reserved scopes from the set and return the result. * @param scopes * @return */ - public Set removeRestrictedScopes(Set scopes); + public Set removeReservedScopes(Set scopes); + } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/DefaultClientUserDetailsService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/DefaultClientUserDetailsService.java index bb4190fd04..da7a177c87 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/DefaultClientUserDetailsService.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/DefaultClientUserDetailsService.java @@ -1,24 +1,31 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; -import java.util.ArrayList; +import java.math.BigInteger; +import java.security.SecureRandom; import java.util.Collection; +import java.util.HashSet; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -26,57 +33,71 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import org.springframework.stereotype.Service; import com.google.common.base.Strings; /** * Shim layer to convert a ClientDetails service into a UserDetails service - * + * * @author AANGANES * */ @Service("clientUserDetailsService") public class DefaultClientUserDetailsService implements UserDetailsService { + private static GrantedAuthority ROLE_CLIENT = new SimpleGrantedAuthority("ROLE_CLIENT"); + + @Autowired + private ClientDetailsEntityService clientDetailsService; + @Autowired - private ClientDetailsService clientDetailsService; + private ConfigurationPropertiesBean config; @Override public UserDetails loadUserByUsername(String clientId) throws UsernameNotFoundException { - ClientDetails client = clientDetailsService.loadClientByClientId(clientId); - - if (client != null) { - - String password = Strings.nullToEmpty(client.getClientSecret()); - boolean enabled = true; - boolean accountNonExpired = true; - boolean credentialsNonExpired = true; - boolean accountNonLocked = true; - Collection authorities = client.getAuthorities(); - if (authorities == null || authorities.isEmpty()) { - // automatically inject ROLE_CLIENT if none exists ... - // TODO: this should probably happen on the client service side instead to keep it in the real data model - authorities = new ArrayList(); - GrantedAuthority roleClient = new SimpleGrantedAuthority("ROLE_CLIENT"); - authorities.add(roleClient); - } + try { + ClientDetailsEntity client = clientDetailsService.loadClientByClientId(clientId); + + if (client != null) { + + String password = Strings.nullToEmpty(client.getClientSecret()); + + if (config.isHeartMode() || // if we're running HEART mode turn off all client secrets + (client.getTokenEndpointAuthMethod() != null && + (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY) || + client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)))) { - return new User(clientId, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); - } else { + // Issue a random password each time to prevent password auth from being used (or skipped) + // for private key or shared key clients, see #715 + + password = new BigInteger(512, new SecureRandom()).toString(16); + } + + boolean enabled = true; + boolean accountNonExpired = true; + boolean credentialsNonExpired = true; + boolean accountNonLocked = true; + Collection authorities = new HashSet<>(client.getAuthorities()); + authorities.add(ROLE_CLIENT); + + return new User(clientId, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + } else { + throw new UsernameNotFoundException("Client not found: " + clientId); + } + } catch (InvalidClientException e) { throw new UsernameNotFoundException("Client not found: " + clientId); } } - public ClientDetailsService getClientDetailsService() { + public ClientDetailsEntityService getClientDetailsService() { return clientDetailsService; } - public void setClientDetailsService(ClientDetailsService clientDetailsService) { + public void setClientDetailsService(ClientDetailsEntityService clientDetailsService) { this.clientDetailsService = clientDetailsService; } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/UriEncodedClientUserDetailsService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/UriEncodedClientUserDetailsService.java new file mode 100644 index 0000000000..64ef7e45cf --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/UriEncodedClientUserDetailsService.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service.impl; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Collection; +import java.util.HashSet; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriUtils; + +import com.google.common.base.Strings; + +/** + * Loads client details based on URI encoding as passed in from basic auth. + * + * Should only get called if non-encoded provider fails. + * + * @author AANGANES + * + */ +@Service("uriEncodedClientUserDetailsService") +public class UriEncodedClientUserDetailsService implements UserDetailsService { + + private static GrantedAuthority ROLE_CLIENT = new SimpleGrantedAuthority("ROLE_CLIENT"); + + @Autowired + private ClientDetailsEntityService clientDetailsService; + + @Autowired + private ConfigurationPropertiesBean config; + + @Override + public UserDetails loadUserByUsername(String clientId) throws UsernameNotFoundException { + + try { + String decodedClientId = UriUtils.decode(clientId, "UTF-8"); + + ClientDetailsEntity client = clientDetailsService.loadClientByClientId(decodedClientId); + + if (client != null) { + + String encodedPassword = UriUtils.encodePathSegment(Strings.nullToEmpty(client.getClientSecret()), "UTF-8"); + + if (config.isHeartMode() || // if we're running HEART mode turn off all client secrets + (client.getTokenEndpointAuthMethod() != null && + (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY) || + client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)))) { + + // Issue a random password each time to prevent password auth from being used (or skipped) + // for private key or shared key clients, see #715 + + encodedPassword = new BigInteger(512, new SecureRandom()).toString(16); + } + + boolean enabled = true; + boolean accountNonExpired = true; + boolean credentialsNonExpired = true; + boolean accountNonLocked = true; + Collection authorities = new HashSet<>(client.getAuthorities()); + authorities.add(ROLE_CLIENT); + + return new User(decodedClientId, encodedPassword, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + } else { + throw new UsernameNotFoundException("Client not found: " + clientId); + } + } catch (InvalidClientException e) { + throw new UsernameNotFoundException("Client not found: " + clientId); + } + + } + + public ClientDetailsEntityService getClientDetailsService() { + return clientDetailsService; + } + + public void setClientDetailsService(ClientDetailsEntityService clientDetailsService) { + this.clientDetailsService = clientDetailsService; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java index fe3f09368b..c07a48da10 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java @@ -1,58 +1,115 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect; -import static org.mitre.discovery.util.JsonUtils.getAsArray; -import static org.mitre.discovery.util.JsonUtils.getAsDate; -import static org.mitre.discovery.util.JsonUtils.getAsJweAlgorithm; -import static org.mitre.discovery.util.JsonUtils.getAsJweEncryptionMethod; -import static org.mitre.discovery.util.JsonUtils.getAsJwsAlgorithm; -import static org.mitre.discovery.util.JsonUtils.getAsString; -import static org.mitre.discovery.util.JsonUtils.getAsStringSet; +import static org.mitre.util.JsonUtils.getAsArray; +import static org.mitre.util.JsonUtils.getAsDate; +import static org.mitre.util.JsonUtils.getAsJweAlgorithm; +import static org.mitre.util.JsonUtils.getAsJweEncryptionMethod; +import static org.mitre.util.JsonUtils.getAsJwsAlgorithm; +import static org.mitre.util.JsonUtils.getAsPkceAlgorithm; +import static org.mitre.util.JsonUtils.getAsString; +import static org.mitre.util.JsonUtils.getAsStringSet; + +import java.text.ParseException; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.ClientDetailsEntity.AppType; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; import org.mitre.oauth2.model.RegisteredClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.Sets; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + +import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.CODE_CHALLENGE_METHOD; +import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE; +import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME; +import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE; +import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE_SEPARATOR; +import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_ID; +import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT; +import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_VERSION; +import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG; /** + * Utility class to handle the parsing and serialization of ClientDetails objects. + * * @author jricher * */ public class ClientDetailsEntityJsonProcessor { + private static Logger logger = LoggerFactory.getLogger(ClientDetailsEntityJsonProcessor.class); + private static JsonParser parser = new JsonParser(); /** - * + * * Create an unbound ClientDetailsEntity from the given JSON string. - * + * * @param jsonString * @return the entity if successful, null otherwise */ @@ -60,85 +117,114 @@ public static ClientDetailsEntity parse(String jsonString) { JsonElement jsonEl = parser.parse(jsonString); return parse(jsonEl); } - + public static ClientDetailsEntity parse(JsonElement jsonEl) { if (jsonEl.isJsonObject()) { JsonObject o = jsonEl.getAsJsonObject(); ClientDetailsEntity c = new ClientDetailsEntity(); - // TODO: make these field names into constants - // these two fields should only be sent in the update request, and MUST match existing values - c.setClientId(getAsString(o, "client_id")); - c.setClientSecret(getAsString(o, "client_secret")); + c.setClientId(getAsString(o, CLIENT_ID)); + c.setClientSecret(getAsString(o, CLIENT_SECRET)); // OAuth DynReg - c.setRedirectUris(getAsStringSet(o, "redirect_uris")); - c.setClientName(getAsString(o, "client_name")); - c.setClientUri(getAsString(o, "client_uri")); - c.setLogoUri(getAsString(o, "logo_uri")); - c.setContacts(getAsStringSet(o, "contacts")); - c.setTosUri(getAsString(o, "tos_uri")); - - String authMethod = getAsString(o, "token_endpoint_auth_method"); + c.setRedirectUris(getAsStringSet(o, REDIRECT_URIS)); + c.setClientName(getAsString(o, CLIENT_NAME)); + c.setClientUri(getAsString(o, CLIENT_URI)); + c.setLogoUri(getAsString(o, LOGO_URI)); + c.setContacts(getAsStringSet(o, CONTACTS)); + c.setTosUri(getAsString(o, TOS_URI)); + + String authMethod = getAsString(o, TOKEN_ENDPOINT_AUTH_METHOD); if (authMethod != null) { c.setTokenEndpointAuthMethod(AuthMethod.getByValue(authMethod)); } // scope is a space-separated string - String scope = getAsString(o, "scope"); + String scope = getAsString(o, SCOPE); if (scope != null) { - c.setScope(Sets.newHashSet(Splitter.on(" ").split(scope))); + c.setScope(Sets.newHashSet(Splitter.on(SCOPE_SEPARATOR).split(scope))); } - c.setGrantTypes(getAsStringSet(o, "grant_types")); - c.setResponseTypes(getAsStringSet(o, "response_types")); - c.setPolicyUri(getAsString(o, "policy_uri")); - c.setJwksUri(getAsString(o, "jwks_uri")); - + c.setGrantTypes(getAsStringSet(o, GRANT_TYPES)); + c.setResponseTypes(getAsStringSet(o, RESPONSE_TYPES)); + c.setPolicyUri(getAsString(o, POLICY_URI)); + c.setJwksUri(getAsString(o, JWKS_URI)); + + JsonElement jwksEl = o.get(JWKS); + if (jwksEl != null && jwksEl.isJsonObject()) { + try { + JWKSet jwks = JWKSet.parse(jwksEl.toString()); // we have to pass this through Nimbus's parser as a string + c.setJwks(jwks); + } catch (ParseException e) { + logger.error("Unable to parse JWK Set for client", e); + return null; + } + } // OIDC Additions - String appType = getAsString(o, "application_type"); + String appType = getAsString(o, APPLICATION_TYPE); if (appType != null) { c.setApplicationType(AppType.getByValue(appType)); } - c.setSectorIdentifierUri(getAsString(o, "sector_identifier_uri")); + c.setSectorIdentifierUri(getAsString(o, SECTOR_IDENTIFIER_URI)); - String subjectType = getAsString(o, "subject_type"); + String subjectType = getAsString(o, SUBJECT_TYPE); if (subjectType != null) { c.setSubjectType(SubjectType.getByValue(subjectType)); } - c.setRequestObjectSigningAlg(getAsJwsAlgorithm(o, "request_object_signing_alg")); + c.setRequestObjectSigningAlg(getAsJwsAlgorithm(o, REQUEST_OBJECT_SIGNING_ALG)); - c.setUserInfoSignedResponseAlg(getAsJwsAlgorithm(o, "userinfo_signed_response_alg")); - c.setUserInfoEncryptedResponseAlg(getAsJweAlgorithm(o, "userinfo_encrypted_response_alg")); - c.setUserInfoEncryptedResponseEnc(getAsJweEncryptionMethod(o, "userinfo_encrypted_response_enc")); + c.setUserInfoSignedResponseAlg(getAsJwsAlgorithm(o, USERINFO_SIGNED_RESPONSE_ALG)); + c.setUserInfoEncryptedResponseAlg(getAsJweAlgorithm(o, USERINFO_ENCRYPTED_RESPONSE_ALG)); + c.setUserInfoEncryptedResponseEnc(getAsJweEncryptionMethod(o, USERINFO_ENCRYPTED_RESPONSE_ENC)); - c.setIdTokenSignedResponseAlg(getAsJwsAlgorithm(o, "id_token_signed_response_alg")); - c.setIdTokenEncryptedResponseAlg(getAsJweAlgorithm(o, "id_token_encrypted_response_alg")); - c.setIdTokenEncryptedResponseEnc(getAsJweEncryptionMethod(o, "id_token_encrypted_response_enc")); + c.setIdTokenSignedResponseAlg(getAsJwsAlgorithm(o, ID_TOKEN_SIGNED_RESPONSE_ALG)); + c.setIdTokenEncryptedResponseAlg(getAsJweAlgorithm(o, ID_TOKEN_ENCRYPTED_RESPONSE_ALG)); + c.setIdTokenEncryptedResponseEnc(getAsJweEncryptionMethod(o, ID_TOKEN_ENCRYPTED_RESPONSE_ENC)); - c.setTokenEndpointAuthSigningAlg(getAsJwsAlgorithm(o, "token_endpoint_auth_signing_alg")); + c.setTokenEndpointAuthSigningAlg(getAsJwsAlgorithm(o, TOKEN_ENDPOINT_AUTH_SIGNING_ALG)); - if (o.has("default_max_age")) { - if (o.get("default_max_age").isJsonPrimitive()) { - c.setDefaultMaxAge(o.get("default_max_age").getAsInt()); + if (o.has(DEFAULT_MAX_AGE)) { + if (o.get(DEFAULT_MAX_AGE).isJsonPrimitive()) { + c.setDefaultMaxAge(o.get(DEFAULT_MAX_AGE).getAsInt()); } } - if (o.has("require_auth_time")) { - if (o.get("require_auth_time").isJsonPrimitive()) { - c.setRequireAuthTime(o.get("require_auth_time").getAsBoolean()); + if (o.has(REQUIRE_AUTH_TIME)) { + if (o.get(REQUIRE_AUTH_TIME).isJsonPrimitive()) { + c.setRequireAuthTime(o.get(REQUIRE_AUTH_TIME).getAsBoolean()); } } - c.setDefaultACRvalues(getAsStringSet(o, "default_acr_values")); - c.setInitiateLoginUri(getAsString(o, "initiate_login_uri")); - c.setPostLogoutRedirectUri(getAsString(o, "post_logout_redirect_uri")); - c.setRequestUris(getAsStringSet(o, "request_uris")); + c.setDefaultACRvalues(getAsStringSet(o, DEFAULT_ACR_VALUES)); + c.setInitiateLoginUri(getAsString(o, INITIATE_LOGIN_URI)); + c.setPostLogoutRedirectUris(getAsStringSet(o, POST_LOGOUT_REDIRECT_URIS)); + c.setRequestUris(getAsStringSet(o, REQUEST_URIS)); + + c.setClaimsRedirectUris(getAsStringSet(o, CLAIMS_REDIRECT_URIS)); + + c.setCodeChallengeMethod(getAsPkceAlgorithm(o, CODE_CHALLENGE_METHOD)); + + c.setSoftwareId(getAsString(o, SOFTWARE_ID)); + c.setSoftwareVersion(getAsString(o, SOFTWARE_VERSION)); + + // note that this does not process or validate the software statement, that's handled in other components + String softwareStatement = getAsString(o, SOFTWARE_STATEMENT); + if (!Strings.isNullOrEmpty(softwareStatement)) { + try { + JWT softwareStatementJwt = JWTParser.parse(softwareStatement); + c.setSoftwareStatement(softwareStatementJwt); + } catch (ParseException e) { + logger.warn("Error parsing software statement", e); + return null; + } + } + + return c; } else { @@ -155,7 +241,7 @@ public static RegisteredClient parseRegistered(String jsonString) { JsonElement jsonEl = parser.parse(jsonString); return parseRegistered(jsonEl); } - + public static RegisteredClient parseRegistered(JsonElement jsonEl) { if (jsonEl.isJsonObject()) { @@ -164,10 +250,12 @@ public static RegisteredClient parseRegistered(JsonElement jsonEl) { RegisteredClient rc = new RegisteredClient(c); // get any fields from the registration - rc.setRegistrationAccessToken(getAsString(o, "registration_access_token")); - rc.setRegistrationClientUri(getAsString(o, "registration_client_uri")); - rc.setClientIdIssuedAt(getAsDate(o, "client_id_issued_at")); - rc.setClientSecretExpiresAt(getAsDate(o, "client_secret_expires_at")); + rc.setRegistrationAccessToken(getAsString(o, REGISTRATION_ACCESS_TOKEN)); + rc.setRegistrationClientUri(getAsString(o, REGISTRATION_CLIENT_URI)); + rc.setClientIdIssuedAt(getAsDate(o, CLIENT_ID_ISSUED_AT)); + rc.setClientSecretExpiresAt(getAsDate(o, CLIENT_SECRET_EXPIRES_AT)); + + rc.setSource(o); return rc; } else { @@ -182,69 +270,96 @@ public static RegisteredClient parseRegistered(JsonElement jsonEl) { * @return */ public static JsonObject serialize(RegisteredClient c) { - JsonObject o = new JsonObject(); - o.addProperty("client_id", c.getClientId()); - if (c.getClientSecret() != null) { - o.addProperty("client_secret", c.getClientSecret()); + if (c.getSource() != null) { + // if we have the original object, just use that + return c.getSource(); + } else { - if (c.getClientSecretExpiresAt() == null) { - o.addProperty("client_secret_expires_at", 0); // TODO: do we want to let secrets expire? - } else { - o.addProperty("client_secret_expires_at", c.getClientSecretExpiresAt().getTime() / 1000L); + JsonObject o = new JsonObject(); + + o.addProperty(CLIENT_ID, c.getClientId()); + if (c.getClientSecret() != null) { + o.addProperty(CLIENT_SECRET, c.getClientSecret()); + + if (c.getClientSecretExpiresAt() == null) { + o.addProperty(CLIENT_SECRET_EXPIRES_AT, 0); // TODO: do we want to let secrets expire? + } else { + o.addProperty(CLIENT_SECRET_EXPIRES_AT, c.getClientSecretExpiresAt().getTime() / 1000L); + } } - } - if (c.getClientIdIssuedAt() != null) { - o.addProperty("client_id_issued_at", c.getClientIdIssuedAt().getTime() / 1000L); - } else if (c.getCreatedAt() != null) { - o.addProperty("client_id_issued_at", c.getCreatedAt().getTime() / 1000L); - } - if (c.getRegistrationAccessToken() != null) { - o.addProperty("registration_access_token", c.getRegistrationAccessToken()); - } + if (c.getClientIdIssuedAt() != null) { + o.addProperty(CLIENT_ID_ISSUED_AT, c.getClientIdIssuedAt().getTime() / 1000L); + } else if (c.getCreatedAt() != null) { + o.addProperty(CLIENT_ID_ISSUED_AT, c.getCreatedAt().getTime() / 1000L); + } + if (c.getRegistrationAccessToken() != null) { + o.addProperty(REGISTRATION_ACCESS_TOKEN, c.getRegistrationAccessToken()); + } - if (c.getRegistrationClientUri() != null) { - o.addProperty("registration_client_uri", c.getRegistrationClientUri()); - } + if (c.getRegistrationClientUri() != null) { + o.addProperty(REGISTRATION_CLIENT_URI, c.getRegistrationClientUri()); + } - // add in all other client properties - - // OAuth DynReg - o.add("redirect_uris", getAsArray(c.getRedirectUris())); - o.addProperty("client_name", c.getClientName()); - o.addProperty("client_uri", c.getClientUri()); - o.addProperty("logo_uri", c.getLogoUri()); - o.add("contacts", getAsArray(c.getContacts())); - o.addProperty("tos_uri", c.getTosUri()); - o.addProperty("token_endpoint_auth_method", c.getTokenEndpointAuthMethod() != null ? c.getTokenEndpointAuthMethod().getValue() : null); - o.addProperty("scope", c.getScope() != null ? Joiner.on(" ").join(c.getScope()) : null); - o.add("grant_types", getAsArray(c.getGrantTypes())); - o.add("response_types", getAsArray(c.getResponseTypes())); - o.addProperty("policy_uri", c.getPolicyUri()); - o.addProperty("jwks_uri", c.getJwksUri()); - - // OIDC Registration - o.addProperty("application_type", c.getApplicationType() != null ? c.getApplicationType().getValue() : null); - o.addProperty("sector_identifier_uri", c.getSectorIdentifierUri()); - o.addProperty("subject_type", c.getSubjectType() != null ? c.getSubjectType().getValue() : null); - o.addProperty("request_object_signing_alg", c.getRequestObjectSigningAlg() != null ? c.getRequestObjectSigningAlg().getName() : null); - o.addProperty("userinfo_signed_response_alg", c.getUserInfoSignedResponseAlg() != null ? c.getUserInfoSignedResponseAlg().getName() : null); - o.addProperty("userinfo_encrypted_response_alg", c.getUserInfoEncryptedResponseAlg() != null ? c.getUserInfoEncryptedResponseAlg().getName() : null); - o.addProperty("userinfo_encrypted_response_enc", c.getUserInfoEncryptedResponseEnc() != null ? c.getUserInfoEncryptedResponseEnc().getName() : null); - o.addProperty("id_token_signed_response_alg", c.getIdTokenSignedResponseAlg() != null ? c.getIdTokenSignedResponseAlg().getName() : null); - o.addProperty("id_token_encrypted_response_alg", c.getIdTokenEncryptedResponseAlg() != null ? c.getIdTokenEncryptedResponseAlg().getName() : null); - o.addProperty("id_token_encrypted_response_enc", c.getIdTokenEncryptedResponseEnc() != null ? c.getIdTokenEncryptedResponseEnc().getName() : null); - o.addProperty("token_endpoint_auth_signing_alg", c.getTokenEndpointAuthSigningAlg() != null ? c.getTokenEndpointAuthSigningAlg().getName() : null); - o.addProperty("default_max_age", c.getDefaultMaxAge()); - o.addProperty("require_auth_time", c.getRequireAuthTime()); - o.add("default_acr_values", getAsArray(c.getDefaultACRvalues())); - o.addProperty("initiate_login_uri", c.getInitiateLoginUri()); - o.addProperty("post_logout_redirect_uri", c.getPostLogoutRedirectUri()); - o.add("request_uris", getAsArray(c.getRequestUris())); - return o; - } + // add in all other client properties + // OAuth DynReg + o.add(REDIRECT_URIS, getAsArray(c.getRedirectUris())); + o.addProperty(CLIENT_NAME, c.getClientName()); + o.addProperty(CLIENT_URI, c.getClientUri()); + o.addProperty(LOGO_URI, c.getLogoUri()); + o.add(CONTACTS, getAsArray(c.getContacts())); + o.addProperty(TOS_URI, c.getTosUri()); + o.addProperty(TOKEN_ENDPOINT_AUTH_METHOD, c.getTokenEndpointAuthMethod() != null ? c.getTokenEndpointAuthMethod().getValue() : null); + o.addProperty(SCOPE, c.getScope() != null ? Joiner.on(SCOPE_SEPARATOR).join(c.getScope()) : null); + o.add(GRANT_TYPES, getAsArray(c.getGrantTypes())); + o.add(RESPONSE_TYPES, getAsArray(c.getResponseTypes())); + o.addProperty(POLICY_URI, c.getPolicyUri()); + o.addProperty(JWKS_URI, c.getJwksUri()); + + // get the JWKS sub-object + if (c.getJwks() != null) { + // We have to re-parse it into GSON because Nimbus uses a different parser + JsonElement jwks = parser.parse(c.getJwks().toString()); + o.add(JWKS, jwks); + } else { + o.add(JWKS, null); + } + // OIDC Registration + o.addProperty(APPLICATION_TYPE, c.getApplicationType() != null ? c.getApplicationType().getValue() : null); + o.addProperty(SECTOR_IDENTIFIER_URI, c.getSectorIdentifierUri()); + o.addProperty(SUBJECT_TYPE, c.getSubjectType() != null ? c.getSubjectType().getValue() : null); + o.addProperty(REQUEST_OBJECT_SIGNING_ALG, c.getRequestObjectSigningAlg() != null ? c.getRequestObjectSigningAlg().getName() : null); + o.addProperty(USERINFO_SIGNED_RESPONSE_ALG, c.getUserInfoSignedResponseAlg() != null ? c.getUserInfoSignedResponseAlg().getName() : null); + o.addProperty(USERINFO_ENCRYPTED_RESPONSE_ALG, c.getUserInfoEncryptedResponseAlg() != null ? c.getUserInfoEncryptedResponseAlg().getName() : null); + o.addProperty(USERINFO_ENCRYPTED_RESPONSE_ENC, c.getUserInfoEncryptedResponseEnc() != null ? c.getUserInfoEncryptedResponseEnc().getName() : null); + o.addProperty(ID_TOKEN_SIGNED_RESPONSE_ALG, c.getIdTokenSignedResponseAlg() != null ? c.getIdTokenSignedResponseAlg().getName() : null); + o.addProperty(ID_TOKEN_ENCRYPTED_RESPONSE_ALG, c.getIdTokenEncryptedResponseAlg() != null ? c.getIdTokenEncryptedResponseAlg().getName() : null); + o.addProperty(ID_TOKEN_ENCRYPTED_RESPONSE_ENC, c.getIdTokenEncryptedResponseEnc() != null ? c.getIdTokenEncryptedResponseEnc().getName() : null); + o.addProperty(TOKEN_ENDPOINT_AUTH_SIGNING_ALG, c.getTokenEndpointAuthSigningAlg() != null ? c.getTokenEndpointAuthSigningAlg().getName() : null); + o.addProperty(DEFAULT_MAX_AGE, c.getDefaultMaxAge()); + o.addProperty(REQUIRE_AUTH_TIME, c.getRequireAuthTime()); + o.add(DEFAULT_ACR_VALUES, getAsArray(c.getDefaultACRvalues())); + o.addProperty(INITIATE_LOGIN_URI, c.getInitiateLoginUri()); + o.add(POST_LOGOUT_REDIRECT_URIS, getAsArray(c.getPostLogoutRedirectUris())); + o.add(REQUEST_URIS, getAsArray(c.getRequestUris())); + + o.add(CLAIMS_REDIRECT_URIS, getAsArray(c.getClaimsRedirectUris())); + + o.addProperty(CODE_CHALLENGE_METHOD, c.getCodeChallengeMethod() != null ? c.getCodeChallengeMethod().getName() : null); + + o.addProperty(SOFTWARE_ID, c.getSoftwareId()); + o.addProperty(SOFTWARE_VERSION, c.getSoftwareVersion()); + + if (c.getSoftwareStatement() != null) { + o.addProperty(SOFTWARE_STATEMENT, c.getSoftwareStatement().serialize()); + } + + return o; + } + + } } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ConfigurationBeanLocaleResolver.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ConfigurationBeanLocaleResolver.java new file mode 100644 index 0000000000..c351e228a4 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ConfigurationBeanLocaleResolver.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.config; + +import java.util.Locale; +import java.util.TimeZone; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.TimeZoneAwareLocaleContext; +import org.springframework.web.servlet.i18n.AbstractLocaleContextResolver; + +/** + * + * Resolve the server's locale from the injected ConfigurationPropertiesBean. + * + * @author jricher + * + */ +public class ConfigurationBeanLocaleResolver extends AbstractLocaleContextResolver { + + @Autowired + private ConfigurationPropertiesBean config; + + @Override + protected Locale getDefaultLocale() { + if (config.getLocale() != null) { + return config.getLocale(); + } else { + return super.getDefaultLocale(); + } + } + + @Override + public LocaleContext resolveLocaleContext(HttpServletRequest request) { + return new TimeZoneAwareLocaleContext() { + @Override + public Locale getLocale() { + return getDefaultLocale(); + } + @Override + public TimeZone getTimeZone() { + return getDefaultTimeZone(); + } + }; + } + + @Override + public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) { + throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy"); + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ConfigurationPropertiesBean.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ConfigurationPropertiesBean.java index 89657a2bbd..9d286518f1 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ConfigurationPropertiesBean.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ConfigurationPropertiesBean.java @@ -1,60 +1,98 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.config; +import java.util.List; +import java.util.Locale; + import javax.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.util.StringUtils; +import com.google.common.collect.Lists; +import com.google.gson.Gson; + /** * Bean to hold configuration information that must be injected into various parts * of our application. Set all of the properties here, and autowire a reference * to this bean if you need access to any configuration properties. - * + * * @author AANGANES * */ public class ConfigurationPropertiesBean { - private static Logger logger = LoggerFactory.getLogger(ConfigurationPropertiesBean.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ConfigurationPropertiesBean.class); private String issuer; private String topbarTitle; + private String shortTopbarTitle; + private String logoImageUrl; - + private Long regTokenLifeTime; + private Long rqpTokenLifeTime; + + private boolean forceHttps = false; // by default we just log a warning for HTTPS deployment + + private Locale locale = Locale.ENGLISH; // we default to the english translation + + private List languageNamespaces = Lists.newArrayList("messages"); + + private boolean dualClient = false; + + private boolean heartMode = false; + + private boolean allowCompleteDeviceCodeUri = false; + public ConfigurationPropertiesBean() { } /** * Endpoints protected by TLS must have https scheme in the URI. + * @throws HttpsUrlRequiredException */ @PostConstruct - public void checkForHttps() { + public void checkConfigConsistency() { if (!StringUtils.startsWithIgnoreCase(issuer, "https")) { - logger.warn("Configured issuer url is not using https scheme."); + if (this.forceHttps) { + logger.error("Configured issuer url is not using https scheme. Server will be shut down!"); + throw new BeanCreationException("Issuer is not using https scheme as required: " + issuer); + } + else { + logger.warn("\n\n**\n** WARNING: Configured issuer url is not using https scheme.\n**\n\n"); + } + } + + if (languageNamespaces == null || languageNamespaces.isEmpty()) { + logger.error("No configured language namespaces! Text rendering will fail!"); } } @@ -86,6 +124,17 @@ public void setTopbarTitle(String topbarTitle) { this.topbarTitle = topbarTitle; } + /** + * @return If shortTopbarTitle is undefined, returns topbarTitle. + */ + public String getShortTopbarTitle() { + return shortTopbarTitle == null ? topbarTitle : shortTopbarTitle; + } + + public void setShortTopbarTitle(String shortTopbarTitle) { + this.shortTopbarTitle = shortTopbarTitle; + } + /** * @return the logoImageUrl */ @@ -113,4 +162,115 @@ public Long getRegTokenLifeTime() { public void setRegTokenLifeTime(Long regTokenLifeTime) { this.regTokenLifeTime = regTokenLifeTime; } + + /** + * @return the rqpTokenLifeTime + */ + public Long getRqpTokenLifeTime() { + return rqpTokenLifeTime; + } + + /** + * @param rqpTokenLifeTime the rqpTokenLifeTime to set + */ + public void setRqpTokenLifeTime(Long rqpTokenLifeTime) { + this.rqpTokenLifeTime = rqpTokenLifeTime; + } + + public boolean isForceHttps() { + return forceHttps; + } + + public void setForceHttps(boolean forceHttps) { + this.forceHttps = forceHttps; + } + + /** + * @return the locale + */ + public Locale getLocale() { + return locale; + } + + /** + * @param locale the locale to set + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * @return the languageNamespaces + */ + public List getLanguageNamespaces() { + return languageNamespaces; + } + + /** + * @param languageNamespaces the languageNamespaces to set + */ + public void setLanguageNamespaces(List languageNamespaces) { + this.languageNamespaces = languageNamespaces; + } + + /** + * @return true if dual client is configured, otherwise false + */ + public boolean isDualClient() { + if (isHeartMode()) { + return false; // HEART mode is incompatible with dual client mode + } else { + return dualClient; + } + } + + /** + * @param dualClient the dual client configuration + */ + public void setDualClient(boolean dualClient) { + this.dualClient = dualClient; + } + + /** + * Get the list of namespaces as a JSON string, for injection into the JavaScript UI + * @return + */ + public String getLanguageNamespacesString() { + return new Gson().toJson(getLanguageNamespaces()); + } + + /** + * Get the default namespace (first in the nonempty list) + */ + public String getDefaultLanguageNamespace() { + return getLanguageNamespaces().get(0); + } + + /** + * @return the heartMode + */ + public boolean isHeartMode() { + return heartMode; + } + + /** + * @param heartMode the heartMode to set + */ + public void setHeartMode(boolean heartMode) { + this.heartMode = heartMode; + } + + /** + * @return the allowCompleteDeviceCodeUri + */ + public boolean isAllowCompleteDeviceCodeUri() { + return allowCompleteDeviceCodeUri; + } + + /** + * @param allowCompleteDeviceCodeUri the allowCompleteDeviceCodeUri to set + */ + public void setAllowCompleteDeviceCodeUri(boolean allowCompleteDeviceCodeUri) { + this.allowCompleteDeviceCodeUri = allowCompleteDeviceCodeUri; + } } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/JWKSetEditor.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/JWKSetEditor.java new file mode 100644 index 0000000000..03c853498e --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/JWKSetEditor.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.config; + +import java.beans.PropertyEditorSupport; +import java.text.ParseException; + +import com.google.common.base.Strings; +import com.nimbusds.jose.jwk.JWKSet; + +/** + * Allows JWK Set strings to be used in XML configurations. + * + * @author jricher + * + */ +public class JWKSetEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (!Strings.isNullOrEmpty(text)) { + try { + setValue(JWKSet.parse(text)); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } else { + setValue(null); + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java index 01c7e41afa..2325e3fd36 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.config; import java.util.List; @@ -25,16 +26,16 @@ /** - * + * * Container class for a client's view of a server's configuration - * + * * @author nemonik, jricher - * + * */ public class ServerConfiguration { /* - * + * issuer REQUIRED. URL using the https scheme with no query or fragment component that the OP asserts as its Issuer Identifier. authorization_endpoint @@ -205,14 +206,20 @@ OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values private Boolean requireRequestUriRegistration; private String opPolicyUri; private String opTosUri; - private UserInfoTokenMethod userInfoTokenMethod; - + + // + // extensions to the discoverable methods + // + + // how do we send the access token to the userinfo endpoint? + private UserInfoTokenMethod userInfoTokenMethod; + public enum UserInfoTokenMethod { HEADER, FORM, QUERY; } - + /** * @return the authorizationEndpointUri */ @@ -657,7 +664,7 @@ public String getOpTosUri() { public void setOpTosUri(String opTosUri) { this.opTosUri = opTosUri; } - + public String getRevocationEndpointUri() { return revocationEndpointUri; } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/UIConfiguration.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/UIConfiguration.java new file mode 100644 index 0000000000..6e4900640e --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/UIConfiguration.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.config; + +import java.util.Set; + +/** + * + * Bean for UI (front-end) configuration to be read at start-up. + * + * @author jricher + * + */ +public class UIConfiguration { + + private Set jsFiles; + + /** + * @return the jsFiles + */ + public Set getJsFiles() { + return jsFiles; + } + /** + * @param jsFiles the jsFiles to set + */ + public void setJsFiles(Set jsFiles) { + this.jsFiles = jsFiles; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/Address.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/Address.java index 1d8154faf2..81fc308faf 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/Address.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/Address.java @@ -1,233 +1,90 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.model; -import javax.persistence.Basic; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; +import java.io.Serializable; -@Entity -@Table(name="address") -public class Address { - - - private Long id; - private String formatted; - private String streetAddress; - private String locality; - private String region; - private String postalCode; - private String country; +public interface Address extends Serializable { /** - * Empty constructor + * Get the system-specific ID of the Address object + * @return */ - public Address() { - - } + public Long getId(); /** - * @return the formatted address string + * @return the formatted address */ - @Basic - @Column(name = "formatted") - public String getFormatted() { - return formatted; - } + public String getFormatted(); + /** * @param formatted the formatted address to set */ - public void setFormatted(String formatted) { - this.formatted = formatted; - } + public void setFormatted(String formatted); + /** * @return the streetAddress */ - @Basic - @Column(name="street_address") - public String getStreetAddress() { - return streetAddress; - } + public String getStreetAddress(); + /** * @param streetAddress the streetAddress to set */ - public void setStreetAddress(String streetAddress) { - this.streetAddress = streetAddress; - } + public void setStreetAddress(String streetAddress); + /** * @return the locality */ - @Basic - @Column(name = "locality") - public String getLocality() { - return locality; - } + public String getLocality(); + /** * @param locality the locality to set */ - public void setLocality(String locality) { - this.locality = locality; - } + public void setLocality(String locality); + /** * @return the region */ - @Basic - @Column(name = "region") - public String getRegion() { - return region; - } + public String getRegion(); + /** * @param region the region to set */ - public void setRegion(String region) { - this.region = region; - } + public void setRegion(String region); + /** * @return the postalCode */ - @Basic - @Column(name="postal_code") - public String getPostalCode() { - return postalCode; - } + public String getPostalCode(); + /** * @param postalCode the postalCode to set */ - public void setPostalCode(String postalCode) { - this.postalCode = postalCode; - } - /** - * @return the country - */ - @Basic - @Column(name = "country") - public String getCountry() { - return country; - } - /** - * @param country the country to set - */ - public void setCountry(String country) { - this.country = country; - } + public void setPostalCode(String postalCode); /** - * @return the id + * @return the country */ - @Id - @GeneratedValue(strategy=GenerationType.IDENTITY) - @Column(name = "id") - public Long getId() { - return id; - } + public String getCountry(); /** - * @param id the id to set - */ - public void setId(Long id) { - this.id = id; - } - - /* (non-Javadoc) - * @see java.lang.Object#hashCode() - */ - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((country == null) ? 0 : country.hashCode()); - result = prime * result + ((formatted == null) ? 0 : formatted.hashCode()); - result = prime * result + ((id == null) ? 0 : id.hashCode()); - result = prime * result + ((locality == null) ? 0 : locality.hashCode()); - result = prime * result + ((postalCode == null) ? 0 : postalCode.hashCode()); - result = prime * result + ((region == null) ? 0 : region.hashCode()); - result = prime * result + ((streetAddress == null) ? 0 : streetAddress.hashCode()); - return result; - } - - /* (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) + * @param country the country to set */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof Address)) { - return false; - } - Address other = (Address) obj; - if (country == null) { - if (other.country != null) { - return false; - } - } else if (!country.equals(other.country)) { - return false; - } - if (formatted == null) { - if (other.formatted != null) { - return false; - } - } else if (!formatted.equals(other.formatted)) { - return false; - } - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - if (locality == null) { - if (other.locality != null) { - return false; - } - } else if (!locality.equals(other.locality)) { - return false; - } - if (postalCode == null) { - if (other.postalCode != null) { - return false; - } - } else if (!postalCode.equals(other.postalCode)) { - return false; - } - if (region == null) { - if (other.region != null) { - return false; - } - } else if (!region.equals(other.region)) { - return false; - } - if (streetAddress == null) { - if (other.streetAddress != null) { - return false; - } - } else if (!streetAddress.equals(other.streetAddress)) { - return false; - } - return true; - } + public void setCountry(String country); } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java index c02ad4a18b..c8b530c1ef 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java @@ -1,26 +1,26 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.model; import java.util.Date; import java.util.Set; import javax.persistence.Basic; -import javax.persistence.CascadeType; import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; @@ -30,28 +30,30 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.Transient; -import org.mitre.oauth2.model.OAuth2AccessTokenEntity; - -import com.google.common.collect.Sets; - @Entity @Table(name="approved_site") @NamedQueries({ - @NamedQuery(name = "ApprovedSite.getAll", query = "select a from ApprovedSite a"), - @NamedQuery(name = "ApprovedSite.getByUserId", query = "select a from ApprovedSite a where a.userId = :userId"), - @NamedQuery(name = "ApprovedSite.getByClientId", query = "select a from ApprovedSite a where a.clientId = :clientId"), - @NamedQuery(name = "ApprovedSite.getByClientIdAndUserId", query = "select a from ApprovedSite a where a.clientId = :clientId and a.userId = :userId") + @NamedQuery(name = ApprovedSite.QUERY_ALL, query = "select a from ApprovedSite a"), + @NamedQuery(name = ApprovedSite.QUERY_BY_USER_ID, query = "select a from ApprovedSite a where a.userId = :" + ApprovedSite.PARAM_USER_ID), + @NamedQuery(name = ApprovedSite.QUERY_BY_CLIENT_ID, query = "select a from ApprovedSite a where a.clientId = :" + ApprovedSite.PARAM_CLIENT_ID), + @NamedQuery(name = ApprovedSite.QUERY_BY_CLIENT_ID_AND_USER_ID, query = "select a from ApprovedSite a where a.clientId = :" + ApprovedSite.PARAM_CLIENT_ID + " and a.userId = :" + ApprovedSite.PARAM_USER_ID) }) public class ApprovedSite { + public static final String QUERY_BY_CLIENT_ID_AND_USER_ID = "ApprovedSite.getByClientIdAndUserId"; + public static final String QUERY_BY_CLIENT_ID = "ApprovedSite.getByClientId"; + public static final String QUERY_BY_USER_ID = "ApprovedSite.getByUserId"; + public static final String QUERY_ALL = "ApprovedSite.getAll"; + + public static final String PARAM_CLIENT_ID = "clientId"; + public static final String PARAM_USER_ID = "userId"; + // unique id private Long id; @@ -74,12 +76,6 @@ public class ApprovedSite { // this should include all information for what data to access private Set allowedScopes; - // If this AP is a WS, link to the WS - private WhitelistedSite whitelistedSite; - - //Link to any access tokens approved through this stored decision - private Set approvedAccessTokens = Sets.newHashSet(); - /** * Empty constructor */ @@ -207,26 +203,6 @@ public void setTimeoutDate(Date timeoutDate) { this.timeoutDate = timeoutDate; } - /** - * Does this AP entry correspond to a WS? - * @return - */ - @Transient - public Boolean getIsWhitelisted() { - return (whitelistedSite != null); - } - - - @ManyToOne - @JoinColumn(name="whitelisted_site_id") - public WhitelistedSite getWhitelistedSite() { - return whitelistedSite; - } - - public void setWhitelistedSite(WhitelistedSite whitelistedSite) { - this.whitelistedSite = whitelistedSite; - } - /** * Has this approval expired? * @return @@ -245,16 +221,4 @@ public boolean isExpired() { } } - @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) - @JoinColumn(name="approved_site_id") - public Set getApprovedAccessTokens() { - return approvedAccessTokens; - } - - /** - * @param approvedAccessTokens the approvedAccessTokens to set - */ - public void setApprovedAccessTokens(Set approvedAccessTokens) { - this.approvedAccessTokens = approvedAccessTokens; - } } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/BlacklistedSite.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/BlacklistedSite.java index 1fb227cbff..bfa4f47667 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/BlacklistedSite.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/BlacklistedSite.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.model; @@ -36,10 +37,12 @@ @Entity @Table(name="blacklisted_site") @NamedQueries({ - @NamedQuery(name = "BlacklistedSite.getAll", query = "select b from BlacklistedSite b") + @NamedQuery(name = BlacklistedSite.QUERY_ALL, query = "select b from BlacklistedSite b") }) public class BlacklistedSite { + public static final String QUERY_ALL = "BlacklistedSite.getAll"; + // unique id private Long id; diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/CachedImage.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/CachedImage.java new file mode 100644 index 0000000000..b4af45c14b --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/CachedImage.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.model; + +/** + * @author jricher + * + */ +public class CachedImage { + + private byte[] data; + private String contentType; + private long length; + + /** + * @return the data + */ + public byte[] getData() { + return data; + } + /** + * @param data the data to set + */ + public void setData(byte[] data) { + this.data = data; + } + /** + * @return the contentType + */ + public String getContentType() { + return contentType; + } + /** + * @param contentType the contentType to set + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + /** + * @return the length + */ + public long getLength() { + return length; + } + /** + * @param length the length to set + */ + public void setLength(long length) { + this.length = length; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/ClientStat.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/ClientStat.java new file mode 100644 index 0000000000..2b817ee791 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/ClientStat.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.model; + +/** + * @author jricher + * + */ +public class ClientStat { + + private Integer approvedSiteCount; + + /** + * @return the count + */ + public Integer getApprovedSiteCount() { + return approvedSiteCount; + } + + /** + * @param count the count to set + */ + public void setApprovedSiteCount(Integer count) { + this.approvedSiteCount = count; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultAddress.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultAddress.java new file mode 100644 index 0000000000..ecdda8bdb8 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultAddress.java @@ -0,0 +1,259 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.model; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="address") +public class DefaultAddress implements Address { + + private static final long serialVersionUID = -1304880008685206811L; + + private Long id; + private String formatted; + private String streetAddress; + private String locality; + private String region; + private String postalCode; + private String country; + + /** + * Empty constructor + */ + public DefaultAddress() { + + } + + /** + * Copy constructor from an existing address. + * @param address + */ + public DefaultAddress(Address address) { + setFormatted(address.getFormatted()); + setStreetAddress(address.getStreetAddress()); + setLocality(address.getLocality()); + setRegion(address.getRegion()); + setPostalCode(address.getPostalCode()); + setCountry(address.getCountry()); + } + + /** + * @return the formatted address string + */ + @Override + @Basic + @Column(name = "formatted") + public String getFormatted() { + return formatted; + } + /** + * @param formatted the formatted address to set + */ + @Override + public void setFormatted(String formatted) { + this.formatted = formatted; + } + /** + * @return the streetAddress + */ + @Override + @Basic + @Column(name="street_address") + public String getStreetAddress() { + return streetAddress; + } + /** + * @param streetAddress the streetAddress to set + */ + @Override + public void setStreetAddress(String streetAddress) { + this.streetAddress = streetAddress; + } + /** + * @return the locality + */ + @Override + @Basic + @Column(name = "locality") + public String getLocality() { + return locality; + } + /** + * @param locality the locality to set + */ + @Override + public void setLocality(String locality) { + this.locality = locality; + } + /** + * @return the region + */ + @Override + @Basic + @Column(name = "region") + public String getRegion() { + return region; + } + /** + * @param region the region to set + */ + @Override + public void setRegion(String region) { + this.region = region; + } + /** + * @return the postalCode + */ + @Override + @Basic + @Column(name="postal_code") + public String getPostalCode() { + return postalCode; + } + /** + * @param postalCode the postalCode to set + */ + @Override + public void setPostalCode(String postalCode) { + this.postalCode = postalCode; + } + /** + * @return the country + */ + @Override + @Basic + @Column(name = "country") + public String getCountry() { + return country; + } + /** + * @param country the country to set + */ + @Override + public void setCountry(String country) { + this.country = country; + } + + /** + * @return the id + */ + @Override + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((country == null) ? 0 : country.hashCode()); + result = prime * result + ((formatted == null) ? 0 : formatted.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((locality == null) ? 0 : locality.hashCode()); + result = prime * result + ((postalCode == null) ? 0 : postalCode.hashCode()); + result = prime * result + ((region == null) ? 0 : region.hashCode()); + result = prime * result + ((streetAddress == null) ? 0 : streetAddress.hashCode()); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof DefaultAddress)) { + return false; + } + DefaultAddress other = (DefaultAddress) obj; + if (country == null) { + if (other.country != null) { + return false; + } + } else if (!country.equals(other.country)) { + return false; + } + if (formatted == null) { + if (other.formatted != null) { + return false; + } + } else if (!formatted.equals(other.formatted)) { + return false; + } + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + if (locality == null) { + if (other.locality != null) { + return false; + } + } else if (!locality.equals(other.locality)) { + return false; + } + if (postalCode == null) { + if (other.postalCode != null) { + return false; + } + } else if (!postalCode.equals(other.postalCode)) { + return false; + } + if (region == null) { + if (other.region != null) { + return false; + } + } else if (!region.equals(other.region)) { + return false; + } + if (streetAddress == null) { + if (other.streetAddress != null) { + return false; + } + } else if (!streetAddress.equals(other.streetAddress)) { + return false; + } + return true; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java index d15df807a8..8b73f3689e 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java @@ -1,23 +1,30 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.model; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + import javax.persistence.Basic; +import javax.persistence.CascadeType; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -28,17 +35,27 @@ import javax.persistence.OneToOne; import javax.persistence.Table; +import org.mitre.openid.connect.model.convert.JsonObjectStringConverter; + import com.google.gson.JsonObject; +import com.google.gson.JsonParser; @Entity @Table(name="user_info") @NamedQueries({ - @NamedQuery(name="DefaultUserInfo.getAll", query = "select u from DefaultUserInfo u"), - @NamedQuery(name="DefaultUserInfo.getByUsername", query = "select u from DefaultUserInfo u WHERE u.preferredUsername = :username"), - @NamedQuery(name="DefaultUserInfo.getBySubject", query = "select u from DefaultUserInfo u WHERE u.sub = :sub") + @NamedQuery(name=DefaultUserInfo.QUERY_BY_USERNAME, query = "select u from DefaultUserInfo u WHERE u.preferredUsername = :" + DefaultUserInfo.PARAM_USERNAME), + @NamedQuery(name=DefaultUserInfo.QUERY_BY_EMAIL, query = "select u from DefaultUserInfo u WHERE u.email = :" + DefaultUserInfo.PARAM_EMAIL) }) public class DefaultUserInfo implements UserInfo { + public static final String QUERY_BY_USERNAME = "DefaultUserInfo.getByUsername"; + public static final String QUERY_BY_EMAIL = "DefaultUserInfo.getByEmailAddress"; + + public static final String PARAM_USERNAME = "username"; + public static final String PARAM_EMAIL = "email"; + + private static final long serialVersionUID = 6078310513185681918L; + private Long id; private String sub; private String preferredUsername; @@ -57,9 +74,10 @@ public class DefaultUserInfo implements UserInfo { private String locale; private String phoneNumber; private Boolean phoneNumberVerified; - private Address address; + private DefaultAddress address; private String updatedTime; private String birthdate; + private transient JsonObject src; // source JSON if this is loaded remotely /** @@ -353,7 +371,7 @@ public void setPhoneNumberVerified(Boolean phoneNumberVerified) { * @see org.mitre.openid.connect.model.UserInfo#getAddress() */ @Override - @OneToOne + @OneToOne(targetEntity = DefaultAddress.class, cascade = CascadeType.ALL) @JoinColumn(name="address_id") public Address getAddress() { return address; @@ -363,7 +381,11 @@ public Address getAddress() { */ @Override public void setAddress(Address address) { - this.address = address; + if (address != null) { + this.address = new DefaultAddress(address); + } else { + this.address = null; + } } /* (non-Javadoc) * @see org.mitre.openid.connect.model.UserInfo#getUpdatedTime() @@ -401,45 +423,51 @@ public void setBirthdate(String birthdate) { @Override public JsonObject toJson() { - JsonObject obj = new JsonObject(); - - obj.addProperty("sub", this.getSub()); - - obj.addProperty("name", this.getName()); - obj.addProperty("preferred_username", this.getPreferredUsername()); - obj.addProperty("given_name", this.getGivenName()); - obj.addProperty("family_name", this.getFamilyName()); - obj.addProperty("middle_name", this.getMiddleName()); - obj.addProperty("nickname", this.getNickname()); - obj.addProperty("profile", this.getProfile()); - obj.addProperty("picture", this.getPicture()); - obj.addProperty("website", this.getWebsite()); - obj.addProperty("gender", this.getGender()); - obj.addProperty("zone_info", this.getZoneinfo()); - obj.addProperty("locale", this.getLocale()); - obj.addProperty("updated_time", this.getUpdatedTime()); - obj.addProperty("birthdate", this.getBirthdate()); - - obj.addProperty("email", this.getEmail()); - obj.addProperty("email_verified", this.getEmailVerified()); - - obj.addProperty("phone_number", this.getPhoneNumber()); - obj.addProperty("phone_number_verified", this.getPhoneNumberVerified()); - - if (this.getAddress() != null) { - - JsonObject addr = new JsonObject(); - addr.addProperty("formatted", this.getAddress().getFormatted()); - addr.addProperty("street_address", this.getAddress().getStreetAddress()); - addr.addProperty("locality", this.getAddress().getLocality()); - addr.addProperty("region", this.getAddress().getRegion()); - addr.addProperty("postal_code", this.getAddress().getPostalCode()); - addr.addProperty("country", this.getAddress().getCountry()); - - obj.add("address", addr); + if (src == null) { + + JsonObject obj = new JsonObject(); + + obj.addProperty("sub", this.getSub()); + + obj.addProperty("name", this.getName()); + obj.addProperty("preferred_username", this.getPreferredUsername()); + obj.addProperty("given_name", this.getGivenName()); + obj.addProperty("family_name", this.getFamilyName()); + obj.addProperty("middle_name", this.getMiddleName()); + obj.addProperty("nickname", this.getNickname()); + obj.addProperty("profile", this.getProfile()); + obj.addProperty("picture", this.getPicture()); + obj.addProperty("website", this.getWebsite()); + obj.addProperty("gender", this.getGender()); + obj.addProperty("zoneinfo", this.getZoneinfo()); + obj.addProperty("locale", this.getLocale()); + obj.addProperty("updated_at", this.getUpdatedTime()); + obj.addProperty("birthdate", this.getBirthdate()); + + obj.addProperty("email", this.getEmail()); + obj.addProperty("email_verified", this.getEmailVerified()); + + obj.addProperty("phone_number", this.getPhoneNumber()); + obj.addProperty("phone_number_verified", this.getPhoneNumberVerified()); + + if (this.getAddress() != null) { + + JsonObject addr = new JsonObject(); + addr.addProperty("formatted", this.getAddress().getFormatted()); + addr.addProperty("street_address", this.getAddress().getStreetAddress()); + addr.addProperty("locality", this.getAddress().getLocality()); + addr.addProperty("region", this.getAddress().getRegion()); + addr.addProperty("postal_code", this.getAddress().getPostalCode()); + addr.addProperty("country", this.getAddress().getCountry()); + + obj.add("address", addr); + } + + return obj; + } else { + return src; } - return obj; } /** @@ -449,40 +477,41 @@ public JsonObject toJson() { */ public static UserInfo fromJson(JsonObject obj) { DefaultUserInfo ui = new DefaultUserInfo(); + ui.setSource(obj); + + ui.setSub(nullSafeGetString(obj, "sub")); + + ui.setName(nullSafeGetString(obj, "name")); + ui.setPreferredUsername(nullSafeGetString(obj, "preferred_username")); + ui.setGivenName(nullSafeGetString(obj, "given_name")); + ui.setFamilyName(nullSafeGetString(obj, "family_name")); + ui.setMiddleName(nullSafeGetString(obj, "middle_name")); + ui.setNickname(nullSafeGetString(obj, "nickname")); + ui.setProfile(nullSafeGetString(obj, "profile")); + ui.setPicture(nullSafeGetString(obj, "picture")); + ui.setWebsite(nullSafeGetString(obj, "website")); + ui.setGender(nullSafeGetString(obj, "gender")); + ui.setZoneinfo(nullSafeGetString(obj, "zoneinfo")); + ui.setLocale(nullSafeGetString(obj, "locale")); + ui.setUpdatedTime(nullSafeGetString(obj, "updated_at")); + ui.setBirthdate(nullSafeGetString(obj, "birthdate")); + + ui.setEmail(nullSafeGetString(obj, "email")); + ui.setEmailVerified(obj.has("email_verified") && obj.get("email_verified").isJsonPrimitive() ? obj.get("email_verified").getAsBoolean() : null); - ui.setSub(obj.has("sub") ? obj.get("sub").getAsString() : null); - - ui.setName(obj.has("name") ? obj.get("name").getAsString() : null); - ui.setPreferredUsername(obj.has("preferred_username") ? obj.get("preferred_username").getAsString() : null); - ui.setGivenName(obj.has("given_name") ? obj.get("given_name").getAsString() : null); - ui.setFamilyName(obj.has("family_name") ? obj.get("family_name").getAsString() : null); - ui.setMiddleName(obj.has("middle_name") ? obj.get("middle_name").getAsString() : null); - ui.setNickname(obj.has("nickname") ? obj.get("nickname").getAsString() : null); - ui.setProfile(obj.has("profile") ? obj.get("profile").getAsString() : null); - ui.setPicture(obj.has("picture") ? obj.get("picture").getAsString() : null); - ui.setWebsite(obj.has("website") ? obj.get("website").getAsString() : null); - ui.setGender(obj.has("gender") ? obj.get("gender").getAsString() : null); - ui.setZoneinfo(obj.has("zone_info") ? obj.get("zone_info").getAsString() : null); - ui.setLocale(obj.has("locale") ? obj.get("locale").getAsString() : null); - ui.setUpdatedTime(obj.has("updated_time") ? obj.get("updated_time").getAsString() : null); - ui.setBirthdate(obj.has("birthdate") ? obj.get("birthdate").getAsString() : null); - - ui.setEmail(obj.has("email") ? obj.get("email").getAsString() : null); - ui.setEmailVerified(obj.has("email_verified") ? obj.get("email_verified").getAsBoolean() : null); - - ui.setPhoneNumber(obj.has("phone_number") ? obj.get("phone_number").getAsString() : null); - ui.setPhoneNumberVerified(obj.has("phone_number_verified") ? obj.get("phone_number_verified").getAsBoolean() : null); + ui.setPhoneNumber(nullSafeGetString(obj, "phone_number")); + ui.setPhoneNumberVerified(obj.has("phone_number_verified") && obj.get("phone_number_verified").isJsonPrimitive() ? obj.get("phone_number_verified").getAsBoolean() : null); if (obj.has("address") && obj.get("address").isJsonObject()) { JsonObject addr = obj.get("address").getAsJsonObject(); - ui.setAddress(new Address()); + ui.setAddress(new DefaultAddress()); - ui.getAddress().setFormatted(addr.has("formatted") ? addr.get("formatted").getAsString() : null); - ui.getAddress().setStreetAddress(addr.has("street_address") ? addr.get("street_address").getAsString() : null); - ui.getAddress().setLocality(addr.has("locality") ? addr.get("locality").getAsString() : null); - ui.getAddress().setRegion(addr.has("region") ? addr.get("region").getAsString() : null); - ui.getAddress().setPostalCode(addr.has("postal_code") ? addr.get("postal_code").getAsString() : null); - ui.getAddress().setCountry(addr.has("country") ? addr.get("country").getAsString() : null); + ui.getAddress().setFormatted(nullSafeGetString(addr, "formatted")); + ui.getAddress().setStreetAddress(nullSafeGetString(addr, "street_address")); + ui.getAddress().setLocality(nullSafeGetString(addr, "locality")); + ui.getAddress().setRegion(nullSafeGetString(addr, "region")); + ui.getAddress().setPostalCode(nullSafeGetString(addr, "postal_code")); + ui.getAddress().setCountry(nullSafeGetString(addr, "country")); } @@ -490,6 +519,28 @@ public static UserInfo fromJson(JsonObject obj) { return ui; } + /** + * @return the jsonString + */ + @Override + @Basic + @Column(name = "src") + @Convert(converter = JsonObjectStringConverter.class) + public JsonObject getSource() { + return src; + } + + /** + * @param jsonString the jsonString to set + */ + public void setSource(JsonObject src) { + this.src = src; + } + + + private static String nullSafeGetString(JsonObject obj, String field) { + return obj.has(field) && obj.get(field).isJsonPrimitive() ? obj.get(field).getAsString() : null; + } /* (non-Javadoc) * @see java.lang.Object#hashCode() @@ -687,4 +738,25 @@ public boolean equals(Object obj) { return true; } + + /* + * Custom serialization to handle the JSON object + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + if (src == null) { + out.writeObject(null); + } else { + out.writeObject(src.toString()); + } + } + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Object o = in.readObject(); + if (o != null) { + JsonParser parser = new JsonParser(); + src = parser.parse((String)o).getAsJsonObject(); + } + } + } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/OIDCAuthenticationToken.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/OIDCAuthenticationToken.java index 76e81547a3..b31b78045a 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/OIDCAuthenticationToken.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/OIDCAuthenticationToken.java @@ -1,54 +1,58 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.model; -import java.util.ArrayList; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.text.ParseException; import java.util.Collection; -import org.mitre.openid.connect.config.ServerConfiguration; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import com.google.common.collect.ImmutableMap; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; /** - * + * * @author Michael Walsh, Justin Richer - * + * */ public class OIDCAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 22100073066377804L; private final ImmutableMap principal; - private final String idTokenValue; // string representation of the id token private final String accessTokenValue; // string representation of the access token private final String refreshTokenValue; // string representation of the refresh token + private transient JWT idToken; // this needs a custom serializer private final String issuer; // issuer URL (parsed from the id token) private final String sub; // user id (parsed from the id token) - private final transient ServerConfiguration serverConfiguration; // server configuration used to fulfill this token, don't serialize it - private final transient UserInfo userInfo; // user info container, don't serialize it b/c it might be huge and can be re-fetched + private final UserInfo userInfo; // user info container /** * Constructs OIDCAuthenticationToken with a full set of authorities, marking this as authenticated. - * + * * Set to authenticated. - * + * * Constructs a Principal out of the subject and issuer. * @param subject * @param authorities @@ -57,7 +61,7 @@ public class OIDCAuthenticationToken extends AbstractAuthenticationToken { */ public OIDCAuthenticationToken(String subject, String issuer, UserInfo userInfo, Collection authorities, - String idTokenValue, String accessTokenValue, String refreshTokenValue) { + JWT idToken, String accessTokenValue, String refreshTokenValue) { super(authorities); @@ -65,48 +69,17 @@ public OIDCAuthenticationToken(String subject, String issuer, this.userInfo = userInfo; this.sub = subject; this.issuer = issuer; - this.idTokenValue = idTokenValue; + this.idToken = idToken; this.accessTokenValue = accessTokenValue; this.refreshTokenValue = refreshTokenValue; - this.serverConfiguration = null; // we don't need a server config anymore - setAuthenticated(true); } - /** - * Constructs OIDCAuthenticationToken for use as a data shuttle from the filter to the auth provider. - * - * Set to not-authenticated. - * - * Constructs a Principal out of the subject and issuer. - * @param sub - * @param idToken - */ - public OIDCAuthenticationToken(String subject, String issuer, - ServerConfiguration serverConfiguration, - String idTokenValue, String accessTokenValue, String refreshTokenValue) { - - super(new ArrayList(0)); - - this.principal = ImmutableMap.of("sub", subject, "iss", issuer); - this.sub = subject; - this.issuer = issuer; - this.idTokenValue = idTokenValue; - this.accessTokenValue = accessTokenValue; - this.refreshTokenValue = refreshTokenValue; - - this.userInfo = null; // we don't have a UserInfo yet - - this.serverConfiguration = serverConfiguration; - - - setAuthenticated(false); - } /* * (non-Javadoc) - * + * * @see org.springframework.security.core.Authentication#getCredentials() */ @Override @@ -129,8 +102,8 @@ public String getSub() { /** * @return the idTokenValue */ - public String getIdTokenValue() { - return idTokenValue; + public JWT getIdToken() { + return idToken; } /** @@ -147,13 +120,6 @@ public String getRefreshTokenValue() { return refreshTokenValue; } - /** - * @return the serverConfiguration - */ - public ServerConfiguration getServerConfiguration() { - return serverConfiguration; - } - /** * @return the issuer */ @@ -168,5 +134,23 @@ public UserInfo getUserInfo() { return userInfo; } + /* + * Custom serialization to handle the JSON object + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + if (idToken == null) { + out.writeObject(null); + } else { + out.writeObject(idToken.serialize()); + } + } + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException, ParseException { + in.defaultReadObject(); + Object o = in.readObject(); + if (o != null) { + idToken = JWTParser.parse((String)o); + } + } } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/PairwiseIdentifier.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/PairwiseIdentifier.java index de3275c492..3ecf2fefe3 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/PairwiseIdentifier.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/PairwiseIdentifier.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.model; @@ -30,20 +31,26 @@ import javax.persistence.Table; /** - * + * * Holds the generated pairwise identifiers for a user. Can be tied to either a client ID or a sector identifier URL. - * + * * @author jricher * */ @Entity @Table(name = "pairwise_identifier") @NamedQueries({ - @NamedQuery(name="PairwiseIdentifier.getAll", query = "select p from PairwiseIdentifier p"), - @NamedQuery(name="PairwiseIdentifier.getBySectorIdentifier", query = "select p from PairwiseIdentifier p WHERE p.userSub = :sub AND p.sectorIdentifier = :sectorIdentifier") + @NamedQuery(name=PairwiseIdentifier.QUERY_ALL, query = "select p from PairwiseIdentifier p"), + @NamedQuery(name=PairwiseIdentifier.QUERY_BY_SECTOR_IDENTIFIER, query = "select p from PairwiseIdentifier p WHERE p.userSub = :" + PairwiseIdentifier.PARAM_SUB + " AND p.sectorIdentifier = :" + PairwiseIdentifier.PARAM_SECTOR_IDENTIFIER) }) public class PairwiseIdentifier { + public static final String QUERY_BY_SECTOR_IDENTIFIER = "PairwiseIdentifier.getBySectorIdentifier"; + public static final String QUERY_ALL = "PairwiseIdentifier.getAll"; + + public static final String PARAM_SECTOR_IDENTIFIER = "sectorIdentifier"; + public static final String PARAM_SUB = "sub"; + private Long id; private String identifier; private String userSub; @@ -86,7 +93,7 @@ public void setIdentifier(String identifier) { * @return the userSub */ @Basic - @Column(name = "sub") + @Column(name = PairwiseIdentifier.PARAM_SUB) public String getUserSub() { return userSub; } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/PendingOIDCAuthenticationToken.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/PendingOIDCAuthenticationToken.java new file mode 100644 index 0000000000..15201a8d4a --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/PendingOIDCAuthenticationToken.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.model; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.text.ParseException; +import java.util.ArrayList; + +import org.mitre.openid.connect.config.ServerConfiguration; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import com.google.common.collect.ImmutableMap; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + +/** + * AuthenticationToken for use as a data shuttle from the filter to the auth provider. + * + * @author jricher + * + */ +public class PendingOIDCAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = 22100073066377804L; + + private final ImmutableMap principal; + private final String accessTokenValue; // string representation of the access token + private final String refreshTokenValue; // string representation of the refresh token + private transient JWT idToken; // this needs a custom serializer + private final String issuer; // issuer URL (parsed from the id token) + private final String sub; // user id (parsed from the id token) + + private final transient ServerConfiguration serverConfiguration; // server configuration used to fulfill this token, don't serialize it + + /** + * Constructs OIDCAuthenticationToken for use as a data shuttle from the filter to the auth provider. + * + * Set to not-authenticated. + * + * Constructs a Principal out of the subject and issuer. + * @param sub + * @param idToken + */ + public PendingOIDCAuthenticationToken (String subject, String issuer, + ServerConfiguration serverConfiguration, + JWT idToken, String accessTokenValue, String refreshTokenValue) { + + super(new ArrayList(0)); + + this.principal = ImmutableMap.of("sub", subject, "iss", issuer); + this.sub = subject; + this.issuer = issuer; + this.idToken = idToken; + this.accessTokenValue = accessTokenValue; + this.refreshTokenValue = refreshTokenValue; + + this.serverConfiguration = serverConfiguration; + + + setAuthenticated(false); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.core.Authentication#getCredentials() + */ + @Override + public Object getCredentials() { + return accessTokenValue; + } + + /** + * Get the principal of this object, an immutable map of the subject and issuer. + */ + @Override + public Object getPrincipal() { + return principal; + } + + public String getSub() { + return sub; + } + + /** + * @return the idTokenValue + */ + public JWT getIdToken() { + return idToken; + } + + /** + * @return the accessTokenValue + */ + public String getAccessTokenValue() { + return accessTokenValue; + } + + /** + * @return the refreshTokenValue + */ + public String getRefreshTokenValue() { + return refreshTokenValue; + } + + /** + * @return the serverConfiguration + */ + public ServerConfiguration getServerConfiguration() { + return serverConfiguration; + } + + /** + * @return the issuer + */ + public String getIssuer() { + return issuer; + } + + /* + * Custom serialization to handle the JSON object + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + if (idToken == null) { + out.writeObject(null); + } else { + out.writeObject(idToken.serialize()); + } + } + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException, ParseException { + in.defaultReadObject(); + Object o = in.readObject(); + if (o != null) { + idToken = JWTParser.parse((String)o); + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java index ce18a53033..2fbac5e402 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java @@ -1,235 +1,244 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.model; +import java.io.Serializable; + import com.google.gson.JsonObject; -public interface UserInfo { +public interface UserInfo extends Serializable { /** * @return the userId */ - public abstract String getSub(); + public String getSub(); /** * @param sub the userId to set */ - public abstract void setSub(String sub); + public void setSub(String sub); /** * @return the preferred username */ - public abstract String getPreferredUsername(); + public String getPreferredUsername(); /** * @param preferredUsername the preferredUsername to set */ - public abstract void setPreferredUsername(String preferredUsername); + public void setPreferredUsername(String preferredUsername); /** * @return the name */ - public abstract String getName(); + public String getName(); /** * @param name the name to set */ - public abstract void setName(String name); + public void setName(String name); /** * @return the givenName */ - public abstract String getGivenName(); + public String getGivenName(); /** * @param givenName the givenName to set */ - public abstract void setGivenName(String givenName); + public void setGivenName(String givenName); /** * @return the familyName */ - public abstract String getFamilyName(); + public String getFamilyName(); /** * @param familyName the familyName to set */ - public abstract void setFamilyName(String familyName); + public void setFamilyName(String familyName); /** * @return the middleName */ - public abstract String getMiddleName(); + public String getMiddleName(); /** * @param middleName the middleName to set */ - public abstract void setMiddleName(String middleName); + public void setMiddleName(String middleName); /** * @return the nickname */ - public abstract String getNickname(); + public String getNickname(); /** * @param nickname the nickname to set */ - public abstract void setNickname(String nickname); + public void setNickname(String nickname); /** * @return the profile */ - public abstract String getProfile(); + public String getProfile(); /** * @param profile the profile to set */ - public abstract void setProfile(String profile); + public void setProfile(String profile); /** * @return the picture */ - public abstract String getPicture(); + public String getPicture(); /** * @param picture the picture to set */ - public abstract void setPicture(String picture); + public void setPicture(String picture); /** * @return the website */ - public abstract String getWebsite(); + public String getWebsite(); /** * @param website the website to set */ - public abstract void setWebsite(String website); + public void setWebsite(String website); /** * @return the email */ - public abstract String getEmail(); + public String getEmail(); /** * @param email the email to set */ - public abstract void setEmail(String email); + public void setEmail(String email); /** * @return the verified */ - public abstract Boolean getEmailVerified(); + public Boolean getEmailVerified(); /** * @param verified the verified to set */ - public abstract void setEmailVerified(Boolean emailVerified); + public void setEmailVerified(Boolean emailVerified); /** * @return the gender */ - public abstract String getGender(); + public String getGender(); /** * @param gender the gender to set */ - public abstract void setGender(String gender); + public void setGender(String gender); /** * @return the zoneinfo */ - public abstract String getZoneinfo(); + public String getZoneinfo(); /** * @param zoneinfo the zoneinfo to set */ - public abstract void setZoneinfo(String zoneinfo); + public void setZoneinfo(String zoneinfo); /** * @return the locale */ - public abstract String getLocale(); + public String getLocale(); /** * @param locale the locale to set */ - public abstract void setLocale(String locale); + public void setLocale(String locale); /** * @return the phoneNumber */ - public abstract String getPhoneNumber(); + public String getPhoneNumber(); /** * @param phoneNumber the phoneNumber to set */ - public abstract void setPhoneNumber(String phoneNumber); + public void setPhoneNumber(String phoneNumber); /** - * + * */ - public abstract Boolean getPhoneNumberVerified(); + public Boolean getPhoneNumberVerified(); /** - * + * * @param phoneNumberVerified */ - public abstract void setPhoneNumberVerified(Boolean phoneNumberVerified); + public void setPhoneNumberVerified(Boolean phoneNumberVerified); /** * @return the address */ - public abstract Address getAddress(); + public Address getAddress(); /** * @param address the address to set */ - public abstract void setAddress(Address address); + public void setAddress(Address address); /** * @return the updatedTime */ - public abstract String getUpdatedTime(); + public String getUpdatedTime(); /** * @param updatedTime the updatedTime to set */ - public abstract void setUpdatedTime(String updatedTime); + public void setUpdatedTime(String updatedTime); /** - * + * * @return */ - public abstract String getBirthdate(); + public String getBirthdate(); /** - * + * * @param birthdate */ - public abstract void setBirthdate(String birthdate); + public void setBirthdate(String birthdate); + + /** + * Serialize this UserInfo object to JSON. + * + * @return + */ + public JsonObject toJson(); /** - * Serialize this UserInfo object to JSON - * + * The JSON source of this UserInfo (if it was fetched), or null if it's local. * @return */ - public abstract JsonObject toJson(); + public JsonObject getSource(); } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java index b8ecafc48a..c3e58db0d3 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.model; import java.util.Set; @@ -41,12 +42,19 @@ @Entity @Table(name="whitelisted_site") @NamedQueries({ - @NamedQuery(name = "WhitelistedSite.getAll", query = "select w from WhitelistedSite w"), - @NamedQuery(name = "WhitelistedSite.getByClientId", query = "select w from WhitelistedSite w where w.clientId = :clientId"), - @NamedQuery(name = "WhitelistedSite.getByCreatoruserId", query = "select w from WhitelistedSite w where w.creatorUserId = :userId") + @NamedQuery(name = WhitelistedSite.QUERY_ALL, query = "select w from WhitelistedSite w"), + @NamedQuery(name = WhitelistedSite.QUERY_BY_CLIENT_ID, query = "select w from WhitelistedSite w where w.clientId = :" + WhitelistedSite.PARAM_CLIENT_ID), + @NamedQuery(name = WhitelistedSite.QUERY_BY_CREATOR, query = "select w from WhitelistedSite w where w.creatorUserId = :" + WhitelistedSite.PARAM_USER_ID) }) public class WhitelistedSite { + public static final String QUERY_BY_CREATOR = "WhitelistedSite.getByCreatoruserId"; + public static final String QUERY_BY_CLIENT_ID = "WhitelistedSite.getByClientId"; + public static final String QUERY_ALL = "WhitelistedSite.getAll"; + + public static final String PARAM_USER_ID = "userId"; + public static final String PARAM_CLIENT_ID = "clientId"; + // unique id private Long id; diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/convert/JsonObjectStringConverter.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/convert/JsonObjectStringConverter.java new file mode 100644 index 0000000000..78c33e8cdd --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/convert/JsonObjectStringConverter.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import com.google.common.base.Strings; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * @author jricher + * + */ +@Converter +public class JsonObjectStringConverter implements AttributeConverter { + + private JsonParser parser = new JsonParser(); + + @Override + public String convertToDatabaseColumn(JsonObject attribute) { + if (attribute != null) { + return attribute.toString(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public JsonObject convertToEntityAttribute(String dbData) { + if (!Strings.isNullOrEmpty(dbData)) { + return parser.parse(dbData).getAsJsonObject(); + } else { + return null; + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/AddressRepository.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/AddressRepository.java index 290a9ed4ba..4482b38234 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/AddressRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/AddressRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository; import org.mitre.openid.connect.model.Address; @@ -28,35 +29,11 @@ public interface AddressRepository { /** * Returns the Address for the given id - * + * * @param id * id the id of the Address * @return a valid Address if it exists, null otherwise */ public Address getById(Long id); - /** - * Removes the given Address from the repository - * - * @param address - * the Address object to remove - */ - public void remove(Address address); - - /** - * Removes an Address from the repository - * - * @param id - * the id of the Address to remove - */ - public void removeById(Long id); - - /** - * Persists a Address - * - * @param address - * the Address to be saved - * @return - */ - public Address save(Address address); } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java index a2633a145c..21e06d97e9 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository; import java.util.Collection; @@ -30,7 +31,7 @@ public interface ApprovedSiteRepository { /** * Returns the ApprovedSite for the given id - * + * * @param id * id the id of the ApprovedSite * @return a valid ApprovedSite if it exists, null otherwise @@ -39,7 +40,7 @@ public interface ApprovedSiteRepository { /** * Return a collection of all ApprovedSites managed by this repository - * + * * @return the ApprovedSite collection, or null */ public Collection getAll(); @@ -47,7 +48,7 @@ public interface ApprovedSiteRepository { /** * Return a collection of ApprovedSite managed by this repository matching the * provided client ID and user ID - * + * * @param clientId * @param userId * @return @@ -56,7 +57,7 @@ public interface ApprovedSiteRepository { /** * Removes the given ApprovedSite from the repository - * + * * @param aggregator * the ApprovedSite object to remove */ @@ -64,7 +65,7 @@ public interface ApprovedSiteRepository { /** * Persists an ApprovedSite - * + * * @param aggregator * valid ApprovedSite instance * @return the persisted entity diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/BlacklistedSiteRepository.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/BlacklistedSiteRepository.java index 335439a25c..9c491c3900 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/BlacklistedSiteRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/BlacklistedSiteRepository.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository; diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/PairwiseIdentifierRepository.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/PairwiseIdentifierRepository.java index e15da0a0ca..b17850b459 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/PairwiseIdentifierRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/PairwiseIdentifierRepository.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.repository; @@ -29,7 +30,7 @@ public interface PairwiseIdentifierRepository { /** * Get a pairwise identifier by its associated user subject and sector identifier. - * + * * @param sub * @param sectorIdentifierUri * @return @@ -38,7 +39,7 @@ public interface PairwiseIdentifierRepository { /** * Save a pairwise identifier to the database. - * + * * @param pairwise */ public void save(PairwiseIdentifier pairwise); diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java index 115ad25aff..9763ca14a5 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java @@ -1,70 +1,46 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository; -import java.util.Collection; - import org.mitre.openid.connect.model.UserInfo; /** * UserInfo repository interface - * + * * @author Michael Joseph Walsh * */ public interface UserInfoRepository { /** - * Returns the UserInfo for the given subject - * - * @param sub - * the subject of the UserInfo - * @return a valid UserInfo if it exists, null otherwise - */ - public UserInfo getBySubject(String sub); - - /** - * Persists a UserInfo - * - * @param user + * Get a UserInfo object by its preferred_username field + * @param username * @return */ - public UserInfo save(UserInfo userInfo); - - /** - * Removes the given UserInfo from the repository - * - * @param userInfo - * the UserInfo object to remove - */ - public void remove(UserInfo userInfo); - - /** - * Return a collection of all UserInfos managed by this repository - * - * @return the UserInfo collection, or null - */ - public Collection getAll(); + public UserInfo getByUsername(String username); /** - * Get a UserInfo object by its preferred_username field - * @param username + * + * Get the UserInfo object by its email field + * + * @param email * @return */ - public UserInfo getByUsername(String username); + public UserInfo getByEmailAddress(String email); } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java index 3cc2550e7e..b46ec5d273 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository; import java.util.Collection; @@ -22,22 +23,22 @@ /** * WhitelistedSite repository interface - * + * * @author Michael Joseph Walsh, aanganes - * + * */ public interface WhitelistedSiteRepository { /** * Return a collection of all WhitelistedSite managed by this repository - * + * * @return the WhitelistedSite collection, or null */ public Collection getAll(); /** * Returns the WhitelistedSite for the given id - * + * * @param id * id the id of the WhitelistedSite * @return a valid WhitelistedSite if it exists, null otherwise @@ -46,7 +47,7 @@ public interface WhitelistedSiteRepository { /** * Find a WhitelistedSite by its associated ClientDetails reference - * + * * @param client the Relying Party * @return the corresponding WhitelistedSite if one exists for the RP, or null */ @@ -54,7 +55,7 @@ public interface WhitelistedSiteRepository { /** * Return a collection of the WhitelistedSites created by a given user - * + * * @param creator the id of the admin who may have created some WhitelistedSites * @return the collection of corresponding WhitelistedSites, if any, or null */ @@ -62,7 +63,7 @@ public interface WhitelistedSiteRepository { /** * Removes the given IdToken from the repository - * + * * @param whitelistedSite * the WhitelistedSite object to remove */ @@ -70,7 +71,7 @@ public interface WhitelistedSiteRepository { /** * Persists a WhitelistedSite - * + * * @param whitelistedSite * @return */ diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ApprovedSiteService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ApprovedSiteService.java index 4f8270bfaa..bf033d8874 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ApprovedSiteService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ApprovedSiteService.java @@ -1,43 +1,45 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.Set; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.openid.connect.model.ApprovedSite; -import org.mitre.openid.connect.model.WhitelistedSite; import org.springframework.security.oauth2.provider.ClientDetails; /** * Interface for ApprovedSite service - * + * * @author Michael Joseph Walsh, aanganes - * + * */ public interface ApprovedSiteService { - public ApprovedSite createApprovedSite(String clientId, String userId, Date timeoutDate, Set allowedScopes, WhitelistedSite whitelistedSite); + public ApprovedSite createApprovedSite(String clientId, String userId, Date timeoutDate, Set allowedScopes); /** * Return a collection of all ApprovedSites - * + * * @return the ApprovedSite collection, or null */ public Collection getAll(); @@ -45,7 +47,7 @@ public interface ApprovedSiteService { /** * Return a collection of ApprovedSite managed by this repository matching the * provided client ID and user ID - * + * * @param clientId * @param userId * @return @@ -54,7 +56,7 @@ public interface ApprovedSiteService { /** * Save an ApprovedSite - * + * * @param approvedSite * the ApprovedSite to be saved */ @@ -62,7 +64,7 @@ public interface ApprovedSiteService { /** * Get ApprovedSite for id - * + * * @param id * id for ApprovedSite * @return ApprovedSite for id, or null @@ -71,7 +73,7 @@ public interface ApprovedSiteService { /** * Remove the ApprovedSite - * + * * @param approvedSite * the ApprovedSite to remove */ @@ -102,4 +104,11 @@ public interface ApprovedSiteService { * @return */ public void clearExpiredSites(); + + /** + * Return all approved access tokens for the site. + * @return + */ + public List getApprovedAccessTokens(ApprovedSite approvedSite); + } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/BlacklistedSiteService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/BlacklistedSiteService.java index 529f429e70..88ef7bff7d 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/BlacklistedSiteService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/BlacklistedSiteService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service; diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ClientLogoLoadingService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ClientLogoLoadingService.java new file mode 100644 index 0000000000..92cfd67eca --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ClientLogoLoadingService.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.openid.connect.model.CachedImage; + +/** + * @author jricher + * + */ +public interface ClientLogoLoadingService { + + /** + * @param client + * @return + */ + public CachedImage getLogo(ClientDetailsEntity client); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/LoginHintExtracter.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/LoginHintExtracter.java new file mode 100644 index 0000000000..f83894bf29 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/LoginHintExtracter.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service; + +/** + * @author jricher + * + */ +public interface LoginHintExtracter { + + /** + * @param loginHint + * @return + */ + public String extractHint(String loginHint); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataService.java new file mode 100644 index 0000000000..ce85762799 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataService.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.service; + +import java.io.IOException; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * @author jricher + * @author arielak + */ +public interface MITREidDataService { + + /** + * Data member for 1.X configurations + */ + public static final String MITREID_CONNECT_1_0 = "mitreid-connect-1.0"; + public static final String MITREID_CONNECT_1_1 = "mitreid-connect-1.1"; + public static final String MITREID_CONNECT_1_2 = "mitreid-connect-1.2"; + public static final String MITREID_CONNECT_1_3 = "mitreid-connect-1.3"; + + // member names + public static final String REFRESHTOKENS = "refreshTokens"; + public static final String ACCESSTOKENS = "accessTokens"; + public static final String WHITELISTEDSITES = "whitelistedSites"; + public static final String BLACKLISTEDSITES = "blacklistedSites"; + public static final String AUTHENTICATIONHOLDERS = "authenticationHolders"; + public static final String GRANTS = "grants"; + public static final String CLIENTS = "clients"; + public static final String SYSTEMSCOPES = "systemScopes"; + + /** + * Write out the current server state to the given JSON writer as a JSON object + * + * @param writer + * @throws IOException + */ + void exportData(JsonWriter writer) throws IOException; + + /** + * Read in the current server state from the given JSON reader as a JSON object + * + * @param reader + */ + void importData(JsonReader reader) throws IOException; + + /** + * Return true if the this data service supports the given version. This is called before + * handing the service the reader through its importData function. + * + * @param version + * @return + */ + boolean supportsVersion(String version); + +} \ No newline at end of file diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataServiceExtension.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataServiceExtension.java new file mode 100644 index 0000000000..b42dafe9e9 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataServiceExtension.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service; + +import java.io.IOException; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * A modular extension to the data import/export layer. Any instances of this need to be + * declared as beans to be picked up by the data services. + * + * @author jricher + * + */ +public interface MITREidDataServiceExtension { + + /** + * Export any data for this extension. This is called from the top level object. + * All extensions MUST return the writer to a state such that another member of + * the top level object can be written next. + * + * @param writer + */ + void exportExtensionData(JsonWriter writer) throws IOException; + + /** + * Import data that's part of this extension. This is called from the context of + * reading the top level object. All extensions MUST return the reader to a state + * such that another member of the top level object can be read next. The name of + * the data element being imported is passed in as name. If the extension does not + * support this data element, it must return without advancing the reader. + * + * Returns "true" if the item was processed, "false" otherwise. + * + * @param reader + */ + boolean importExtensionData(String name, JsonReader reader) throws IOException; + + /** + * Signal the extension to wrap up all object processing and finalize its + */ + void fixExtensionObjectReferences(MITREidDataServiceMaps maps); + + /** + * Return + * @param mitreidConnect13 + * @return + */ + boolean supportsVersion(String version); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataServiceMaps.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataServiceMaps.java new file mode 100644 index 0000000000..38e5fb46a9 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/MITREidDataServiceMaps.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author jricher + * + */ +public class MITREidDataServiceMaps { + + private Map accessTokenOldToNewIdMap = new HashMap(); + private Map accessTokenToAuthHolderRefs = new HashMap(); + private Map accessTokenToClientRefs = new HashMap(); + private Map accessTokenToRefreshTokenRefs = new HashMap(); + private Map authHolderOldToNewIdMap = new HashMap(); + private Map grantOldToNewIdMap = new HashMap<>(); + private Map> grantToAccessTokensRefs = new HashMap<>(); + private Map refreshTokenOldToNewIdMap = new HashMap(); + private Map refreshTokenToAuthHolderRefs = new HashMap(); + private Map refreshTokenToClientRefs = new HashMap(); + private Map whitelistedSiteOldToNewIdMap = new HashMap(); + /** + * @return the accessTokenOldToNewIdMap + */ + public Map getAccessTokenOldToNewIdMap() { + return accessTokenOldToNewIdMap; + } + /** + * @return the accessTokenToAuthHolderRefs + */ + public Map getAccessTokenToAuthHolderRefs() { + return accessTokenToAuthHolderRefs; + } + /** + * @return the accessTokenToClientRefs + */ + public Map getAccessTokenToClientRefs() { + return accessTokenToClientRefs; + } + /** + * @return the accessTokenToRefreshTokenRefs + */ + public Map getAccessTokenToRefreshTokenRefs() { + return accessTokenToRefreshTokenRefs; + } + /** + * @return the authHolderOldToNewIdMap + */ + public Map getAuthHolderOldToNewIdMap() { + return authHolderOldToNewIdMap; + } + /** + * @return the grantOldToNewIdMap + */ + public Map getGrantOldToNewIdMap() { + return grantOldToNewIdMap; + } + /** + * @return the grantToAccessTokensRefs + */ + public Map> getGrantToAccessTokensRefs() { + return grantToAccessTokensRefs; + } + /** + * @return the refreshTokenOldToNewIdMap + */ + public Map getRefreshTokenOldToNewIdMap() { + return refreshTokenOldToNewIdMap; + } + /** + * @return the refreshTokenToAuthHolderRefs + */ + public Map getRefreshTokenToAuthHolderRefs() { + return refreshTokenToAuthHolderRefs; + } + /** + * @return the refreshTokenToClientRefs + */ + public Map getRefreshTokenToClientRefs() { + return refreshTokenToClientRefs; + } + /** + * @return the whitelistedSiteOldToNewIdMap + */ + public Map getWhitelistedSiteOldToNewIdMap() { + return whitelistedSiteOldToNewIdMap; + } + + public void clearAll() { + refreshTokenToClientRefs.clear(); + refreshTokenToAuthHolderRefs.clear(); + accessTokenToClientRefs.clear(); + accessTokenToAuthHolderRefs.clear(); + accessTokenToRefreshTokenRefs.clear(); + refreshTokenOldToNewIdMap.clear(); + accessTokenOldToNewIdMap.clear(); + grantOldToNewIdMap.clear(); + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/OIDCTokenService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/OIDCTokenService.java index ecb0f9a954..146f6164e4 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/OIDCTokenService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/OIDCTokenService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service; import java.util.Date; @@ -22,9 +23,11 @@ import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.springframework.security.oauth2.provider.OAuth2Request; +import com.nimbusds.jwt.JWT; + /** * Service to create specialty OpenID Connect tokens. - * + * * @author Amanda Anganes * */ @@ -32,7 +35,7 @@ public interface OIDCTokenService { /** * Create an id token with the information provided. - * + * * @param client * @param request * @param issueTime @@ -41,13 +44,13 @@ public interface OIDCTokenService { * @param accessToken * @return */ - public OAuth2AccessTokenEntity createIdToken( + public JWT createIdToken( ClientDetailsEntity client, OAuth2Request request, Date issueTime, String sub, OAuth2AccessTokenEntity accessToken); /** * Create a registration access token for the given client. - * + * * @param client * @return */ @@ -55,10 +58,17 @@ public OAuth2AccessTokenEntity createIdToken( /** * Create a resource access token for the given client (protected resource). - * + * * @param client * @return */ public OAuth2AccessTokenEntity createResourceAccessToken(ClientDetailsEntity client); + /** + * Rotate the registration or resource token for a client + * @param client + * @return + */ + public OAuth2AccessTokenEntity rotateRegistrationAccessTokenForClient(ClientDetailsEntity client); + } \ No newline at end of file diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/PairwiseIdentiferService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/PairwiseIdentiferService.java index c70faa07da..06f437812e 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/PairwiseIdentiferService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/PairwiseIdentiferService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.service; @@ -30,9 +31,9 @@ public interface PairwiseIdentiferService { /** * Calcualtes the pairwise identifier for the given userinfo object and client. - * + * * Returns 'null' if no identifer could be calculated. - * + * * @param userInfo * @param client * @return diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java index d1c4bb1031..d07d888e0f 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.service; @@ -31,6 +32,4 @@ public interface ScopeClaimTranslationService { public Set getClaimsForScopeSet(Set scopes); - public String getFieldNameForClaim(String claim); - -} \ No newline at end of file +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/StatsService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/StatsService.java index 7ca425350a..007e3e76c6 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/StatsService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/StatsService.java @@ -1,26 +1,29 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service; import java.util.Map; +import org.mitre.openid.connect.model.ClientStat; + /** * @author jricher * @@ -32,25 +35,18 @@ public interface StatsService { * approvalCount: total approved sites * userCount: unique users * clientCount: unique clients - * + * * @return */ public Map getSummaryStats(); - /** - * Calculate usage count for all clients - * - * @return a map of id of client object to number of approvals - */ - public Map getByClientId(); - /** * Calculate the usage count for a single client - * - * @param id the id of the client to search on + * + * @param clientId the id of the client to search on * @return */ - public Integer getCountForClientId(Long id); + public ClientStat getCountForClientId(String clientId); /** * Trigger the stats to be recalculated upon next update. diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/UserInfoService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/UserInfoService.java index f43379ff6e..4a36c236ca 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/UserInfoService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/UserInfoService.java @@ -1,57 +1,32 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service; import org.mitre.openid.connect.model.UserInfo; /** * Interface for UserInfo service - * + * * @author Michael Joseph Walsh - * + * */ public interface UserInfoService { - /** - * Save an UserInfo - * - * @param userInfo - * the UserInfo to be saved - */ - public void save(UserInfo userInfo); - - /** - * Get UserInfo for the Subject - * - * @param sub - * subject for UserInfo - * @return UserInfo for sub, or null - */ - public UserInfo getBySubject(String userId); - - /** - * Remove the UserInfo - * - * @param userInfo - * the UserInfo to remove - */ - public void remove(UserInfo userInfo); - - /** * Get the UserInfo for the given username (usually maps to the * preferredUsername field). @@ -70,4 +45,12 @@ public interface UserInfoService { */ public UserInfo getByUsernameAndClientId(String username, String clientId); + /** + * Get the user registered at this server with the given email address. + * + * @param email + * @return + */ + public UserInfo getByEmailAddress(String email); + } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/WhitelistedSiteService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/WhitelistedSiteService.java index dce92c0e74..c4780ef87c 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/WhitelistedSiteService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/WhitelistedSiteService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service; import java.util.Collection; @@ -22,22 +23,22 @@ /** * Interface for WhitelistedSite service - * + * * @author Michael Joseph Walsh, aanganes - * + * */ public interface WhitelistedSiteService { /** * Return a collection of all WhitelistedSite managed by this service - * + * * @return the WhitelistedSite collection, or null */ public Collection getAll(); /** * Returns the WhitelistedSite for the given id - * + * * @param id * id the id of the WhitelistedSite * @return a valid WhitelistedSite if it exists, null otherwise @@ -46,23 +47,17 @@ public interface WhitelistedSiteService { /** * Find a WhitelistedSite by its associated ClientDetails reference - * + * * @param client the Relying Party * @return the corresponding WhitelistedSite if one exists for the RP, or null */ public WhitelistedSite getByClientId(String clientId); - /** - * Return a collection of the WhitelistedSites created by a given user - * - * @param creator the user id of an admin who may have made some WhitelistedSites - * @return the collection of corresponding WhitelistedSites, if any, or null - */ - public Collection getByCreator(String creatorId); + /** * Removes the given WhitelistedSite from the repository - * + * * @param address * the WhitelistedSite object to remove */ @@ -70,7 +65,7 @@ public interface WhitelistedSiteService { /** * Persists a new WhitelistedSite - * + * * @param whitelistedSite * the WhitelistedSite to be saved * @return diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/view/JwkKeyListView.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/view/JWKSetView.java similarity index 72% rename from openid-connect-common/src/main/java/org/mitre/openid/connect/view/JwkKeyListView.java rename to openid-connect-common/src/main/java/org/mitre/openid/connect/view/JWKSetView.java index b07e765b6f..f18deaee18 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/view/JwkKeyListView.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/view/JWKSetView.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -29,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.servlet.view.AbstractView; @@ -39,21 +41,25 @@ * @author jricher * */ -@Component("jwkKeyList") -public class JwkKeyListView extends AbstractView { +@Component(JWKSetView.VIEWNAME) +public class JWKSetView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(JwkKeyListView.class); + public static final String VIEWNAME = "jwkSet"; + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JWKSetView.class); @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); //BiMap keyMap = (BiMap) model.get("keys"); Map keys = (Map) model.get("keys"); - JWKSet jwkSet = new JWKSet(new ArrayList(keys.values())); + JWKSet jwkSet = new JWKSet(new ArrayList<>(keys.values())); try { @@ -62,7 +68,7 @@ protected void renderMergedOutputModel(Map model, HttpServletReq } catch (IOException e) { - logger.error("IOException in JwkKeyListView.java: ", e); + logger.error("IOException in JWKSetView.java: ", e); } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java index cf1bc75149..ac7ab41070 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java @@ -1,26 +1,26 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.lang.reflect.Type; -import java.security.Principal; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -29,11 +29,12 @@ import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.service.UserInfoService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; -import org.springframework.web.servlet.ModelAndView; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; -import org.springframework.web.servlet.view.RedirectView; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -44,71 +45,63 @@ /** * Injects the UserInfo object for the current user into the current model's context, if both exist. Allows JSPs and the like to call "userInfo.name" and other fields. - * + * * @author jricher * */ public class UserInfoInterceptor extends HandlerInterceptorAdapter { private Gson gson = new GsonBuilder() - .registerTypeHierarchyAdapter(GrantedAuthority.class, new JsonSerializer() { - @Override - public JsonElement serialize(GrantedAuthority src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.getAuthority()); - } - }) - .create(); + .registerTypeHierarchyAdapter(GrantedAuthority.class, new JsonSerializer() { + @Override + public JsonElement serialize(GrantedAuthority src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getAuthority()); + } + }) + .create(); @Autowired (required = false) private UserInfoService userInfoService; - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { - - if (modelAndView != null && !modelAndView.getModel().containsKey("userInfo")) { // skip checking at all if we have no model and view to hand the user to - // or if there's already a userInfo object in there + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - // TODO: this is a patch to get around a potential information leak from #492 - if (!(modelAndView.getView() instanceof RedirectView)) { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - // get our principal from the security context - Principal p = request.getUserPrincipal(); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (p instanceof Authentication && !modelAndView.getModel().containsKey("userAuthorities")){ - Authentication auth = (Authentication)p; - modelAndView.addObject("userAuthorities", gson.toJson(auth.getAuthorities())); - } + if (auth instanceof Authentication){ + request.setAttribute("userAuthorities", gson.toJson(auth.getAuthorities())); + } - if (p instanceof OIDCAuthenticationToken) { - // if they're logging into this server from a remote OIDC server, pass through their user info - OIDCAuthenticationToken oidc = (OIDCAuthenticationToken) p; - if (oidc.getUserInfo() != null) { - modelAndView.addObject("userInfo", oidc.getUserInfo()); - modelAndView.addObject("userInfoJson", oidc.getUserInfo().toJson()); - } else { - modelAndView.addObject("userInfo", null); - modelAndView.addObject("userInfoJson", "null"); - } + if (!trustResolver.isAnonymous(auth)) { // skip lookup on anonymous logins + if (auth instanceof OIDCAuthenticationToken) { + // if they're logging into this server from a remote OIDC server, pass through their user info + OIDCAuthenticationToken oidc = (OIDCAuthenticationToken) auth; + if (oidc.getUserInfo() != null) { + request.setAttribute("userInfo", oidc.getUserInfo()); + request.setAttribute("userInfoJson", oidc.getUserInfo().toJson()); } else { - // don't bother checking if we don't have a principal or a userInfoService to work with - if (p != null && p.getName() != null && userInfoService != null) { + request.setAttribute("userInfo", null); + request.setAttribute("userInfoJson", "null"); + } + } else { + // don't bother checking if we don't have a principal or a userInfoService to work with + if (auth != null && auth.getName() != null && userInfoService != null) { - // try to look up a user based on the principal's name - UserInfo user = userInfoService.getByUsername(p.getName()); + // try to look up a user based on the principal's name + UserInfo user = userInfoService.getByUsername(auth.getName()); - // if we have one, inject it so views can use it - if (user != null) { - modelAndView.addObject("userInfo", user); - modelAndView.addObject("userInfoJson", user.toJson()); - } + // if we have one, inject it so views can use it + if (user != null) { + request.setAttribute("userInfo", user); + request.setAttribute("userInfoJson", user.toJson()); } } } } + return true; } - - - } diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/Claim.java b/openid-connect-common/src/main/java/org/mitre/uma/model/Claim.java new file mode 100644 index 0000000000..d93e99c9c7 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/Claim.java @@ -0,0 +1,256 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model; + +import java.util.Set; + +import javax.persistence.Basic; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Table; + +import org.mitre.oauth2.model.convert.JsonElementStringConverter; + +import com.google.gson.JsonElement; + +/** + * @author jricher + * + */ +@Entity +@Table(name = "claim") +public class Claim { + + private Long id; + private String name; + private String friendlyName; + private String claimType; + private JsonElement value; + private Set claimTokenFormat; + private Set issuer; + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + /** + * @return the name + */ + @Basic + @Column(name = "name") + public String getName() { + return name; + } + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the friendlyName + */ + @Basic + @Column(name = "friendly_name") + public String getFriendlyName() { + return friendlyName; + } + /** + * @param friendlyName the friendlyName to set + */ + public void setFriendlyName(String friendlyName) { + this.friendlyName = friendlyName; + } + + /** + * @return the claimType + */ + @Basic + @Column(name = "claim_type") + public String getClaimType() { + return claimType; + } + /** + * @param claimType the claimType to set + */ + public void setClaimType(String claimType) { + this.claimType = claimType; + } + + /** + * @return the claimTokenFormat + */ + @ElementCollection(fetch = FetchType.EAGER) + @Column(name = "claim_token_format") + @CollectionTable( + name = "claim_token_format", + joinColumns = @JoinColumn(name = "owner_id") + ) + public Set getClaimTokenFormat() { + return claimTokenFormat; + } + /** + * @param claimTokenFormat the claimTokenFormat to set + */ + public void setClaimTokenFormat(Set claimTokenFormat) { + this.claimTokenFormat = claimTokenFormat; + } + + /** + * @return the issuer + */ + @ElementCollection(fetch = FetchType.EAGER) + @Column(name = "issuer") + @CollectionTable( + name = "claim_issuer", + joinColumns = @JoinColumn(name = "owner_id") + ) + public Set getIssuer() { + return issuer; + } + /** + * @param issuer the issuer to set + */ + public void setIssuer(Set issuer) { + this.issuer = issuer; + } + + /** + * @return the value + */ + @Basic + @Column(name = "claim_value") + @Convert(converter = JsonElementStringConverter.class) + public JsonElement getValue() { + return value; + } + /** + * @param value the value to set + */ + public void setValue(JsonElement value) { + this.value = value; + } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Claim [id=" + id + ", name=" + name + ", friendlyName=" + friendlyName + ", claimType=" + claimType + ", value=" + value + ", claimTokenFormat=" + claimTokenFormat + ", issuer=" + issuer + "]"; + } + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((claimTokenFormat == null) ? 0 : claimTokenFormat.hashCode()); + result = prime * result + ((claimType == null) ? 0 : claimType.hashCode()); + result = prime * result + ((friendlyName == null) ? 0 : friendlyName.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((issuer == null) ? 0 : issuer.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Claim other = (Claim) obj; + if (claimTokenFormat == null) { + if (other.claimTokenFormat != null) { + return false; + } + } else if (!claimTokenFormat.equals(other.claimTokenFormat)) { + return false; + } + if (claimType == null) { + if (other.claimType != null) { + return false; + } + } else if (!claimType.equals(other.claimType)) { + return false; + } + if (friendlyName == null) { + if (other.friendlyName != null) { + return false; + } + } else if (!friendlyName.equals(other.friendlyName)) { + return false; + } + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + if (issuer == null) { + if (other.issuer != null) { + return false; + } + } else if (!issuer.equals(other.issuer)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + if (value == null) { + if (other.value != null) { + return false; + } + } else if (!value.equals(other.value)) { + return false; + } + return true; + } +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/ClaimProcessingResult.java b/openid-connect-common/src/main/java/org/mitre/uma/model/ClaimProcessingResult.java new file mode 100644 index 0000000000..0c00653307 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/ClaimProcessingResult.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model; + +import java.util.Collection; + +/** + * Data shuttle to return results of the claims processing service. + * + * @author jricher + * + */ +public class ClaimProcessingResult { + + private boolean satisfied; + private Collection unmatched; + private Policy matched; + + /** + * Create an unmatched result. isSatisfied is false. + * @param unmatched + */ + public ClaimProcessingResult(Collection unmatched) { + this.satisfied = false; + this.unmatched = unmatched; + this.matched = null; + } + + /** + * Create a matched result. isSatisfied is true. + * @param matched + */ + public ClaimProcessingResult(Policy matched) { + this.satisfied = true; + this.matched = matched; + this.unmatched = null; + } + + /** + * @return the satisfied + */ + public boolean isSatisfied() { + return satisfied; + } + + /** + * @param satisfied the satisfied to set + */ + public void setSatisfied(boolean satisfied) { + this.satisfied = satisfied; + } + + /** + * @return the unmatched + */ + public Collection getUnmatched() { + return unmatched; + } + + /** + * @param unmatched the unmatched to set + */ + public void setUnmatched(Collection unmatched) { + this.unmatched = unmatched; + } + + /** + * @return the matched + */ + public Policy getMatched() { + return matched; + } + + /** + * @param matched the matched to set + */ + public void setMatched(Policy matched) { + this.matched = matched; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/Permission.java b/openid-connect-common/src/main/java/org/mitre/uma/model/Permission.java new file mode 100644 index 0000000000..42fd2d7f7f --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/Permission.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model; + +import java.util.Set; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +/** + * @author jricher + */ +@Entity +@Table(name = "permission") +public class Permission { + + private Long id; + private ResourceSet resourceSet; + private Set scopes; + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the resourceSet + */ + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "resource_set_id") + public ResourceSet getResourceSet() { + return resourceSet; + } + + /** + * @param resourceSet the resourceSet to set + */ + public void setResourceSet(ResourceSet resourceSet) { + this.resourceSet = resourceSet; + } + + /** + * @return the scopes + */ + @ElementCollection(fetch = FetchType.EAGER) + @Column(name = "scope") + @CollectionTable( + name = "permission_scope", + joinColumns = @JoinColumn(name = "owner_id") + ) + public Set getScopes() { + return scopes; + } + + /** + * @param scopes the scopes to set + */ + public void setScopes(Set scopes) { + this.scopes = scopes; + } +} \ No newline at end of file diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/PermissionTicket.java b/openid-connect-common/src/main/java/org/mitre/uma/model/PermissionTicket.java new file mode 100644 index 0000000000..8b89ef5fda --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/PermissionTicket.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model; + +import java.util.Collection; +import java.util.Date; + +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +/** + * + * An UMA permission, used in the protection API. + * + * @author jricher + * + */ +@Entity +@Table(name = "permission_ticket") +@NamedQueries({ + @NamedQuery(name = PermissionTicket.QUERY_TICKET, query = "select p from PermissionTicket p where p.ticket = :" + PermissionTicket.PARAM_TICKET), + @NamedQuery(name = PermissionTicket.QUERY_ALL, query = "select p from PermissionTicket p"), + @NamedQuery(name = PermissionTicket.QUERY_BY_RESOURCE_SET, query = "select p from PermissionTicket p where p.permission.resourceSet.id = :" + PermissionTicket.PARAM_RESOURCE_SET_ID) +}) +public class PermissionTicket { + + public static final String QUERY_TICKET = "PermissionTicket.queryByTicket"; + public static final String QUERY_ALL = "PermissionTicket.queryAll"; + public static final String QUERY_BY_RESOURCE_SET = "PermissionTicket.queryByResourceSet"; + + public static final String PARAM_TICKET = "ticket"; + public static final String PARAM_RESOURCE_SET_ID = "rsid"; + + private Long id; + private Permission permission; + private String ticket; + private Date expiration; + private Collection claimsSupplied; + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the permission + */ + @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinColumn(name = "permission_id") + public Permission getPermission() { + return permission; + } + + /** + * @param permission the permission to set + */ + public void setPermission(Permission permission) { + this.permission = permission; + } + + /** + * @return the ticket + */ + @Basic + @Column(name = "ticket") + public String getTicket() { + return ticket; + } + + /** + * @param ticket the ticket to set + */ + public void setTicket(String ticket) { + this.ticket = ticket; + } + + /** + * @return the expiration + */ + @Basic + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "expiration") + public Date getExpiration() { + return expiration; + } + + /** + * @param expiration the expiration to set + */ + public void setExpiration(Date expiration) { + this.expiration = expiration; + } + + /** + * @return the claimsSupplied + */ + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinTable( + name = "claim_to_permission_ticket", + joinColumns = @JoinColumn(name = "permission_ticket_id"), + inverseJoinColumns = @JoinColumn(name = "claim_id") + ) + public Collection getClaimsSupplied() { + return claimsSupplied; + } + + /** + * @param claimsSupplied the claimsSupplied to set + */ + public void setClaimsSupplied(Collection claimsSupplied) { + this.claimsSupplied = claimsSupplied; + } + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/Policy.java b/openid-connect-common/src/main/java/org/mitre/uma/model/Policy.java new file mode 100644 index 0000000000..32098e2fb1 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/Policy.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model; + +import java.util.Collection; +import java.util.Set; + +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +/** + * A set of claims required to fulfill a given permission. + * + * @author jricher + * + */ +@Entity +@Table(name = "policy") +public class Policy { + + private Long id; + private String name; + private Collection claimsRequired; + private Set scopes; + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the name + */ + @Basic + @Column(name = "name") + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the claimsRequired + */ + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinTable( + name = "claim_to_policy", + joinColumns = @JoinColumn(name = "policy_id"), + inverseJoinColumns = @JoinColumn(name = "claim_id") + ) + public Collection getClaimsRequired() { + return claimsRequired; + } + + /** + * @param claimsRequired the claimsRequired to set + */ + public void setClaimsRequired(Collection claimsRequired) { + this.claimsRequired = claimsRequired; + } + + /** + * @return the scopes + */ + @ElementCollection(fetch = FetchType.EAGER) + @Column(name = "scope") + @CollectionTable( + name = "policy_scope", + joinColumns = @JoinColumn(name = "owner_id") + ) + public Set getScopes() { + return scopes; + } + + /** + * @param scopes the scopes to set + */ + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Policy [id=" + id + ", name=" + name + ", claimsRequired=" + claimsRequired + ", scopes=" + scopes + "]"; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((claimsRequired == null) ? 0 : claimsRequired.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((scopes == null) ? 0 : scopes.hashCode()); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Policy other = (Policy) obj; + if (claimsRequired == null) { + if (other.claimsRequired != null) { + return false; + } + } else if (!claimsRequired.equals(other.claimsRequired)) { + return false; + } + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + if (scopes == null) { + if (other.scopes != null) { + return false; + } + } else if (!scopes.equals(other.scopes)) { + return false; + } + return true; + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/ResourceSet.java b/openid-connect-common/src/main/java/org/mitre/uma/model/ResourceSet.java new file mode 100644 index 0000000000..6303d377f2 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/ResourceSet.java @@ -0,0 +1,329 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name = "resource_set") +@NamedQueries ({ + @NamedQuery(name = ResourceSet.QUERY_BY_OWNER, query = "select r from ResourceSet r where r.owner = :" + ResourceSet.PARAM_OWNER), + @NamedQuery(name = ResourceSet.QUERY_BY_OWNER_AND_CLIENT, query = "select r from ResourceSet r where r.owner = :" + ResourceSet.PARAM_OWNER + " and r.clientId = :" + ResourceSet.PARAM_CLIENTID), + @NamedQuery(name = ResourceSet.QUERY_BY_CLIENT, query = "select r from ResourceSet r where r.clientId = :" + ResourceSet.PARAM_CLIENTID), + @NamedQuery(name = ResourceSet.QUERY_ALL, query = "select r from ResourceSet r") +}) +public class ResourceSet { + + public static final String QUERY_BY_OWNER = "ResourceSet.queryByOwner"; + public static final String QUERY_BY_OWNER_AND_CLIENT = "ResourceSet.queryByOwnerAndClient"; + public static final String QUERY_BY_CLIENT = "ResourceSet.queryByClient"; + public static final String QUERY_ALL = "ResourceSet.queryAll"; + + public static final String PARAM_OWNER = "owner"; + public static final String PARAM_CLIENTID = "clientId"; + + private Long id; + private String name; + private String uri; + private String type; + private Set scopes = new HashSet<>(); + private String iconUri; + + private String owner; // username of the person responsible for the registration (either directly or via OAuth token) + private String clientId; // client id of the protected resource that registered this resource set via OAuth token + + private Collection policies = new HashSet<>(); + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the name + */ + @Basic + @Column(name = "name") + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the uri + */ + @Basic + @Column(name = "uri") + public String getUri() { + return uri; + } + + /** + * @param uri the uri to set + */ + public void setUri(String uri) { + this.uri = uri; + } + + /** + * @return the type + */ + @Basic + @Column(name = "rs_type") + public String getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(String type) { + this.type = type; + } + + /** + * @return the scopes + */ + @ElementCollection(fetch = FetchType.EAGER) + @Column(name = "scope") + @CollectionTable( + name = "resource_set_scope", + joinColumns = @JoinColumn(name = "owner_id") + ) + public Set getScopes() { + return scopes; + } + + /** + * @param scopes the scopes to set + */ + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + /** + * @return the iconUri + */ + @Basic + @Column(name = "icon_uri") + public String getIconUri() { + return iconUri; + } + + /** + * @param iconUri the iconUri to set + */ + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + /** + * @return the owner + */ + @Basic + @Column(name = "owner") + public String getOwner() { + return owner; + } + + /** + * @param owner the owner to set + */ + public void setOwner(String owner) { + this.owner = owner; + } + + /** + * @return the clientId + */ + @Basic + @Column(name = "client_id") + public String getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * @return the claimsRequired + */ + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "resource_set_id") + public Collection getPolicies() { + return policies; + } + + /** + * @param policies the claimsRequired to set + */ + public void setPolicies(Collection policies) { + this.policies = policies; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "ResourceSet [id=" + id + ", name=" + name + ", uri=" + uri + ", type=" + type + ", scopes=" + scopes + ", iconUri=" + iconUri + ", owner=" + owner + ", clientId=" + clientId + ", policies=" + policies + "]"; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((clientId == null) ? 0 : clientId.hashCode()); + result = prime * result + ((iconUri == null) ? 0 : iconUri.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((owner == null) ? 0 : owner.hashCode()); + result = prime * result + ((policies == null) ? 0 : policies.hashCode()); + result = prime * result + ((scopes == null) ? 0 : scopes.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ResourceSet other = (ResourceSet) obj; + if (clientId == null) { + if (other.clientId != null) { + return false; + } + } else if (!clientId.equals(other.clientId)) { + return false; + } + if (iconUri == null) { + if (other.iconUri != null) { + return false; + } + } else if (!iconUri.equals(other.iconUri)) { + return false; + } + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + if (owner == null) { + if (other.owner != null) { + return false; + } + } else if (!owner.equals(other.owner)) { + return false; + } + if (policies == null) { + if (other.policies != null) { + return false; + } + } else if (!policies.equals(other.policies)) { + return false; + } + if (scopes == null) { + if (other.scopes != null) { + return false; + } + } else if (!scopes.equals(other.scopes)) { + return false; + } + if (type == null) { + if (other.type != null) { + return false; + } + } else if (!type.equals(other.type)) { + return false; + } + if (uri == null) { + if (other.uri != null) { + return false; + } + } else if (!uri.equals(other.uri)) { + return false; + } + return true; + } + + + + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/SavedRegisteredClient.java b/openid-connect-common/src/main/java/org/mitre/uma/model/SavedRegisteredClient.java new file mode 100644 index 0000000000..4b0ed95551 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/SavedRegisteredClient.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.mitre.oauth2.model.RegisteredClient; +import org.mitre.uma.model.convert.RegisteredClientStringConverter; + +/** + * @author jricher + * + */ +@Entity +@Table(name = "saved_registered_client") +public class SavedRegisteredClient { + + private Long id; + private String issuer; + private RegisteredClient registeredClient; + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + public Long getId() { + return id; + } + + /** + * + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the issuer + */ + @Basic + @Column(name = "issuer") + public String getIssuer() { + return issuer; + } + + /** + * @param issuer the issuer to set + */ + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + /** + * @return the registeredClient + */ + @Basic + @Column(name = "registered_client") + @Convert(converter = RegisteredClientStringConverter.class) + public RegisteredClient getRegisteredClient() { + return registeredClient; + } + + /** + * @param registeredClient the registeredClient to set + */ + public void setRegisteredClient(RegisteredClient registeredClient) { + this.registeredClient = registeredClient; + } + + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/model/convert/RegisteredClientStringConverter.java b/openid-connect-common/src/main/java/org/mitre/uma/model/convert/RegisteredClientStringConverter.java new file mode 100644 index 0000000000..6a68f9da39 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/model/convert/RegisteredClientStringConverter.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import org.mitre.oauth2.model.RegisteredClient; +import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor; + +import com.google.common.base.Strings; + +/** + * @author jricher + * + */ +@Converter +public class RegisteredClientStringConverter implements AttributeConverter{ + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToDatabaseColumn(java.lang.Object) + */ + @Override + public String convertToDatabaseColumn(RegisteredClient attribute) { + if (attribute == null || attribute.getSource() == null) { + return null; + } else { + return attribute.getSource().toString(); + } + + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public RegisteredClient convertToEntityAttribute(String dbData) { + if (Strings.isNullOrEmpty(dbData)) { + return null; + } else { + return ClientDetailsEntityJsonProcessor.parseRegistered(dbData); + } + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/repository/PermissionRepository.java b/openid-connect-common/src/main/java/org/mitre/uma/repository/PermissionRepository.java new file mode 100644 index 0000000000..825453dcb6 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/repository/PermissionRepository.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.repository; + +import java.util.Collection; + +import org.mitre.uma.model.Permission; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; + +/** + * @author jricher + * + */ +public interface PermissionRepository { + + /** + * + * Save a permission ticket. + * + * @param p + * @return + */ + public PermissionTicket save(PermissionTicket p); + + /** + * Get the permission indicated by its ticket value. + * + * @param ticket + * @return + */ + public PermissionTicket getByTicket(String ticket); + + /** + * Get all the tickets in the system (used by the import/export API) + * + * @return + */ + public Collection getAll(); + + /** + * Save a permission object with no associated ticket (used by the import/export API) + * + * @param p + * @return + */ + public Permission saveRawPermission(Permission p); + + /** + * Get a permission object by its ID (used by the import/export API) + * + * @param permissionId + * @return + */ + public Permission getById(Long permissionId); + + /** + * Get all permission tickets issued against a resource set (called when RS is deleted) + * + * @param rs + * @return + */ + public Collection getPermissionTicketsForResourceSet(ResourceSet rs); + + /** + * Remove the specified ticket. + * + * @param ticket + */ + public void remove(PermissionTicket ticket); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/repository/ResourceSetRepository.java b/openid-connect-common/src/main/java/org/mitre/uma/repository/ResourceSetRepository.java new file mode 100644 index 0000000000..7c5a1cb73b --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/repository/ResourceSetRepository.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.repository; + +import java.util.Collection; + +import org.mitre.uma.model.ResourceSet; + +/** + * @author jricher + * + */ +public interface ResourceSetRepository { + + public ResourceSet save(ResourceSet rs); + + public ResourceSet getById(Long id); + + public void remove(ResourceSet rs); + + public Collection getAllForOwner(String owner); + + public Collection getAllForOwnerAndClient(String owner, String clientId); + + public Collection getAll(); + + public Collection getAllForClient(String clientId); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/service/ClaimsProcessingService.java b/openid-connect-common/src/main/java/org/mitre/uma/service/ClaimsProcessingService.java new file mode 100644 index 0000000000..da88c4a7c4 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/service/ClaimsProcessingService.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service; + +import org.mitre.uma.model.ClaimProcessingResult; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; + +/** + * + * Processes claims presented during an UMA transaction. + * + * @author jricher + * + */ +public interface ClaimsProcessingService { + + /** + * + * Determine whether or not the claims that have been supplied are + * sufficient to fulfill the requirements given by the claims that + * are required. + * + * @param rs the required claims to check against + * @param ticket the supplied claims to test + * @return the result of the claims processing action + */ + public ClaimProcessingResult claimsAreSatisfied(ResourceSet rs, PermissionTicket ticket); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/service/PermissionService.java b/openid-connect-common/src/main/java/org/mitre/uma/service/PermissionService.java new file mode 100644 index 0000000000..ab7ea2e3f6 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/service/PermissionService.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service; + +import java.util.Set; + +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; +import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException; + + +/** + * @author jricher + * + */ +public interface PermissionService { + + /** + * @param resourceSet the resource set to create the permission on + * @param scopes the set of scopes that this permission is for + * @return the created (and stored) permission object, with ticket + * @throws InsufficientScopeException if the scopes in scopes don't match those in resourceSet.getScopes + */ + public PermissionTicket createTicket(ResourceSet resourceSet, Set scopes); + + /** + * + * Read the permission associated with the given ticket. + * + * @param the ticket value to search on + * @return the permission object, or null if none is found + */ + public PermissionTicket getByTicket(String ticket); + + /** + * Save the updated permission ticket to the database. Does not create a new ticket. + * + * @param ticket + * @return + */ + public PermissionTicket updateTicket(PermissionTicket ticket); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/service/ResourceSetService.java b/openid-connect-common/src/main/java/org/mitre/uma/service/ResourceSetService.java new file mode 100644 index 0000000000..8da2ce017c --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/service/ResourceSetService.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service; + +import java.util.Collection; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.uma.model.ResourceSet; + +/** + * + * Manage registered resource sets at this authorization server. + * + * @author jricher + * + */ +public interface ResourceSetService { + + public ResourceSet saveNew(ResourceSet rs); + + public ResourceSet getById(Long id); + + public ResourceSet update(ResourceSet oldRs, ResourceSet newRs); + + public void remove(ResourceSet rs); + + public Collection getAllForOwner(String owner); + + public Collection getAllForOwnerAndClient(String owner, String authClientId); + + public Collection getAllForClient(ClientDetailsEntity client); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/service/SavedRegisteredClientService.java b/openid-connect-common/src/main/java/org/mitre/uma/service/SavedRegisteredClientService.java new file mode 100644 index 0000000000..4b8745c961 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/service/SavedRegisteredClientService.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service; + +import java.util.Collection; + +import org.mitre.oauth2.model.RegisteredClient; +import org.mitre.uma.model.SavedRegisteredClient; + +/** + * @author jricher + * + */ +public interface SavedRegisteredClientService { + + /** + * Get a list of all the registered clients that we know about. + * + * @return + */ + Collection getAll(); + + /** + * @param issuer + * @param client + */ + void save(String issuer, RegisteredClient client); + + +} diff --git a/openid-connect-common/src/main/java/org/mitre/uma/service/UmaTokenService.java b/openid-connect-common/src/main/java/org/mitre/uma/service/UmaTokenService.java new file mode 100644 index 0000000000..8ee6c86b15 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/uma/service/UmaTokenService.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.Policy; +import org.springframework.security.oauth2.provider.OAuth2Authentication; + +/** + * Service to create special tokens for UMA. + * + * @author jricher + * + */ +public interface UmaTokenService { + + /** + * Create the RPT from the given authentication and ticket. + * + */ + public OAuth2AccessTokenEntity createRequestingPartyToken(OAuth2Authentication o2auth, PermissionTicket ticket, Policy policy); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/discovery/util/JsonUtils.java b/openid-connect-common/src/main/java/org/mitre/util/JsonUtils.java similarity index 61% rename from openid-connect-common/src/main/java/org/mitre/discovery/util/JsonUtils.java rename to openid-connect-common/src/main/java/org/mitre/util/JsonUtils.java index 1bcf3ec555..b17f150be6 100644 --- a/openid-connect-common/src/main/java/org/mitre/discovery/util/JsonUtils.java +++ b/openid-connect-common/src/main/java/org/mitre/util/JsonUtils.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,43 +16,76 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ -package org.mitre.discovery.util; +package org.mitre.util; +import java.io.IOException; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import org.mitre.oauth2.model.PKCEAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWSAlgorithm; /** * A collection of null-safe converters from common classes and JSON elements, using GSON. - * + * * @author jricher * */ +@SuppressWarnings(value = {"rawtypes", "unchecked"}) public class JsonUtils { + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class); + private static Gson gson = new Gson(); /** - * Translate a set of strings to a JSON array + * Translate a set of strings to a JSON array, empty array returned as null * @param value * @return */ public static JsonElement getAsArray(Set value) { - return gson.toJsonTree(value, new TypeToken>(){}.getType()); + return getAsArray(value, false); + } + + + /** + * Translate a set of strings to a JSON array, optionally preserving the empty array. Otherwise (default) empty array is returned as null. + * @param value + * @param preserveEmpty + * @return + */ + public static JsonElement getAsArray(Set value, boolean preserveEmpty) { + if (!preserveEmpty && value != null && value.isEmpty()) { + // if we're not preserving empty arrays and the value is empty, return null + return JsonNull.INSTANCE; + } else { + return gson.toJsonTree(value, new TypeToken>(){}.getType()); + } } /** @@ -106,6 +140,21 @@ public static JWSAlgorithm getAsJwsAlgorithm(JsonObject o, String member) { } } + /** + * Gets the value of the given member as a PKCE Algorithm, null if it doesn't exist + * @param o + * @param member + * @return + */ + public static PKCEAlgorithm getAsPkceAlgorithm(JsonObject o, String member) { + String s = getAsString(o, member); + if (s != null) { + return PKCEAlgorithm.parse(s); + } else { + return null; + } + } + /** * Gets the value of the given member as a string, null if it doesn't exist */ @@ -138,6 +187,22 @@ public static Boolean getAsBoolean(JsonObject o, String member) { } } + /** + * Gets the value of the given member as a Long, null if it doesn't exist + */ + public static Long getAsLong(JsonObject o, String member) { + if (o.has(member)) { + JsonElement e = o.get(member); + if (e != null && e.isJsonPrimitive()) { + return e.getAsLong(); + } else { + return null; + } + } else { + return null; + } + } + /** * Gets the value of the given given member as a set of strings, null if it doesn't exist */ @@ -174,7 +239,7 @@ public static List getAsStringList(JsonObject o, String member) throws J public static List getAsJwsAlgorithmList(JsonObject o, String member) { List strings = getAsStringList(o, member); if (strings != null) { - List algs = new ArrayList(); + List algs = new ArrayList<>(); for (String alg : strings) { algs.add(JWSAlgorithm.parse(alg)); } @@ -190,7 +255,7 @@ public static List getAsJwsAlgorithmList(JsonObject o, String memb public static List getAsJweAlgorithmList(JsonObject o, String member) { List strings = getAsStringList(o, member); if (strings != null) { - List algs = new ArrayList(); + List algs = new ArrayList<>(); for (String alg : strings) { algs.add(JWEAlgorithm.parse(alg)); } @@ -206,7 +271,7 @@ public static List getAsJweAlgorithmList(JsonObject o, String memb public static List getAsEncryptionMethodList(JsonObject o, String member) { List strings = getAsStringList(o, member); if (strings != null) { - List algs = new ArrayList(); + List algs = new ArrayList<>(); for (String alg : strings) { algs.add(EncryptionMethod.parse(alg)); } @@ -216,4 +281,67 @@ public static List getAsEncryptionMethodList(JsonObject o, Str } } + public static Map readMap(JsonReader reader) throws IOException { + Map map = new HashMap<>(); + reader.beginObject(); + while(reader.hasNext()) { + String name = reader.nextName(); + Object value = null; + switch(reader.peek()) { + case STRING: + value = reader.nextString(); + break; + case BOOLEAN: + value = reader.nextBoolean(); + break; + case NUMBER: + value = reader.nextLong(); + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + map.put(name, value); + } + reader.endObject(); + return map; + } + + public static Set readSet(JsonReader reader) throws IOException { + Set arraySet = null; + reader.beginArray(); + switch (reader.peek()) { + case STRING: + arraySet = new HashSet<>(); + while (reader.hasNext()) { + arraySet.add(reader.nextString()); + } + break; + case NUMBER: + arraySet = new HashSet<>(); + while (reader.hasNext()) { + arraySet.add(reader.nextLong()); + } + break; + default: + arraySet = new HashSet(); + break; + } + reader.endArray(); + return arraySet; + } + + public static void writeNullSafeArray(JsonWriter writer, Set items) throws IOException { + if (items != null) { + writer.beginArray(); + for (String s : items) { + writer.value(s); + } + writer.endArray(); + } else { + writer.nullValue(); + } + } + } diff --git a/openid-connect-common/src/main/java/org/mitre/util/jpa/JpaUtil.java b/openid-connect-common/src/main/java/org/mitre/util/jpa/JpaUtil.java index f60e8b65bb..f15e4c371c 100644 --- a/openid-connect-common/src/main/java/org/mitre/util/jpa/JpaUtil.java +++ b/openid-connect-common/src/main/java/org/mitre/util/jpa/JpaUtil.java @@ -1,24 +1,28 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.util.jpa; import java.util.List; import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; + +import org.mitre.data.PageCriteria; /** * @author mfranklin @@ -28,24 +32,37 @@ public class JpaUtil { public static T getSingleResult(List list) { switch(list.size()) { - case 0: - return null; - case 1: - return list.get(0); - default: - throw new IllegalStateException("Expected single result, got " + list.size()); + case 0: + return null; + case 1: + return list.get(0); + default: + throw new IllegalStateException("Expected single result, got " + list.size()); } } + + /** + * Get a page of results from the specified TypedQuery + * by using the given PageCriteria to limit the query + * results. The PageCriteria will override any size or + * offset already specified on the query. + * + * @param the type parameter + * @param query the query + * @param pageCriteria the page criteria + * @return the list + */ + public static List getResultPage(TypedQuery query, PageCriteria pageCriteria){ + query.setMaxResults(pageCriteria.getPageSize()); + query.setFirstResult(pageCriteria.getPageNumber()*pageCriteria.getPageSize()); + + return query.getResultList(); + } + public static T saveOrUpdate(I id, EntityManager entityManager, T entity) { - if (id == null) { - entityManager.persist(entity); - entityManager.flush(); - return entity; - } else { - T tmp = entityManager.merge(entity); - entityManager.flush(); - return tmp; - } + T tmp = entityManager.merge(entity); + entityManager.flush(); + return tmp; } } diff --git a/openid-connect-common/src/test/java/org/mitre/data/AbstractPageOperationTemplateTest.java b/openid-connect-common/src/test/java/org/mitre/data/AbstractPageOperationTemplateTest.java new file mode 100644 index 0000000000..1bf6d1e3f0 --- /dev/null +++ b/openid-connect-common/src/test/java/org/mitre/data/AbstractPageOperationTemplateTest.java @@ -0,0 +1,231 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.data; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Colm Smyth + */ +public class AbstractPageOperationTemplateTest { + + @Before + public void setUp() throws Exception { + } + + @Test(timeout = 1000L) + public void execute_zeropages() { + CountingPageOperation op = new CountingPageOperation(0,Long.MAX_VALUE); + op.execute(); + + assertEquals(0L, op.counter); + } + + @Test(timeout = 1000L) + public void execute_singlepage() { + CountingPageOperation op = new CountingPageOperation(1,Long.MAX_VALUE); + op.execute(); + + assertEquals(10L, op.counter); + } + + @Test(timeout = 1000L) + public void execute_negpage() { + CountingPageOperation op = new CountingPageOperation(-1,Long.MAX_VALUE); + op.execute(); + + assertEquals(0L, op.counter); + } + + @Test(timeout = 1000L) + public void execute_npage(){ + int n = 7; + CountingPageOperation op = new CountingPageOperation(n,Long.MAX_VALUE); + op.execute(); + + assertEquals(n*10L, op.counter); + } + + @Test(timeout = 1000L) + public void execute_nullpage(){ + CountingPageOperation op = new NullPageCountingPageOperation(Integer.MAX_VALUE, Long.MAX_VALUE); + op.execute(); + + assertEquals(0L, op.getCounter()); + } + + @Test(timeout = 1000L) + public void execute_emptypage(){ + CountingPageOperation op = new EmptyPageCountingPageOperation(Integer.MAX_VALUE, Long.MAX_VALUE); + op.execute(); + + assertEquals(0L, op.getCounter()); + } + + @Test(timeout = 1000L) + public void execute_zerotime(){ + CountingPageOperation op = new CountingPageOperation(Integer.MAX_VALUE,0L); + op.execute(); + + assertEquals(0L, op.getCounter()); + assertEquals(0L, op.getTimeToLastFetch()); + } + + /* + * This is a valid test however it is vulnerable to a race condition + * as such it is being ignored. + */ + @Test(timeout = 1000L) + @Ignore + public void execute_nonzerotime(){ + Long timeMillis = 200L; + CountingPageOperation op = new CountingPageOperation(Integer.MAX_VALUE,timeMillis); + op.execute(); + + assertFalse("last fetch time " + op.getTimeToLastFetch() + "" + + " and previous fetch time " + op.getTimeToPreviousFetch() + + " exceed max time" + timeMillis, + op.getTimeToLastFetch() > timeMillis + && op.getTimeToPreviousFetch() > timeMillis); + } + + @Test(timeout = 1000L) + public void execute_negtime(){ + Long timeMillis = -100L; + CountingPageOperation op = new CountingPageOperation(Integer.MAX_VALUE,timeMillis); + op.execute(); + + assertEquals(0L, op.getCounter()); + } + + @Test(timeout = 1000L) + public void execute_swallowException(){ + CountingPageOperation op = new EvenExceptionCountingPageOperation(1, 1000L); + op.execute(); + + assertTrue(op.isSwallowExceptions()); + assertEquals(5L, op.getCounter()); + } + + @Test(expected = IllegalStateException.class) + public void execute_noSwallowException(){ + CountingPageOperation op = new EvenExceptionCountingPageOperation(1, 1000L); + op.setSwallowExceptions(false); + + try { + op.execute(); + }finally { + assertEquals(1L, op.getCounter()); + } + } + + + private static class CountingPageOperation extends AbstractPageOperationTemplate{ + + private int currentPageFetch; + private int pageSize = 10; + private long counter = 0L; + private long startTime; + private long timeToLastFetch; + private long timeToPreviousFetch; + + private CountingPageOperation(int maxPages, long maxTime) { + super(maxPages, maxTime, "CountingPageOperation"); + startTime = System.currentTimeMillis(); + } + + @Override + public Collection fetchPage() { + timeToPreviousFetch = timeToLastFetch > 0 ? timeToLastFetch : 0; + timeToLastFetch = System.currentTimeMillis() - startTime; + + List page = new ArrayList(pageSize); + for(int i = 0; i < pageSize; i++ ) { + page.add("item " + currentPageFetch * pageSize + i); + } + currentPageFetch++; + return page; + } + + @Override + protected void doOperation(String item) { + counter++; + } + + public long getCounter() { + return counter; + } + + public long getTimeToLastFetch() { + return timeToLastFetch; + } + + public long getTimeToPreviousFetch() { + return timeToPreviousFetch; + } + } + + private static class NullPageCountingPageOperation extends CountingPageOperation { + private NullPageCountingPageOperation(int maxPages, long maxTime) { + super(maxPages, maxTime); + } + + @Override + public Collection fetchPage() { + return null; + } + } + + private static class EmptyPageCountingPageOperation extends CountingPageOperation { + private EmptyPageCountingPageOperation(int maxPages, long maxTime) { + super(maxPages, maxTime); + } + + @Override + public Collection fetchPage() { + return new ArrayList<>(0); + } + } + + private static class EvenExceptionCountingPageOperation extends CountingPageOperation { + + private int callCounter; + private EvenExceptionCountingPageOperation(int maxPages, long maxTime) { + super(maxPages, maxTime); + } + + @Override + protected void doOperation(String item) { + callCounter++; + if(callCounter%2 == 0){ + throw new IllegalStateException("even number items cannot be processed"); + } + + super.doOperation(item); + + } + } +} diff --git a/openid-connect-common/src/test/java/org/mitre/discovery/util/TestWebfingerURLNormalizer.java b/openid-connect-common/src/test/java/org/mitre/discovery/util/TestWebfingerURLNormalizer.java index 4844504781..34b4943603 100644 --- a/openid-connect-common/src/test/java/org/mitre/discovery/util/TestWebfingerURLNormalizer.java +++ b/openid-connect-common/src/test/java/org/mitre/discovery/util/TestWebfingerURLNormalizer.java @@ -1,28 +1,29 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.discovery.util; -import static org.junit.Assert.assertEquals; - import org.junit.Test; import org.springframework.web.util.UriComponents; import com.google.common.collect.ImmutableMap; +import static org.junit.Assert.assertEquals; + /** * @author wkim * diff --git a/openid-connect-common/src/test/java/org/mitre/jose/JOSEEmbedTest.java b/openid-connect-common/src/test/java/org/mitre/jose/JOSEEmbedTest.java deleted file mode 100644 index 53dd77f48e..0000000000 --- a/openid-connect-common/src/test/java/org/mitre/jose/JOSEEmbedTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.jose; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -import com.nimbusds.jose.EncryptionMethod; -import com.nimbusds.jose.JWEAlgorithm; -import com.nimbusds.jose.JWSAlgorithm; - -/** - * - * These tests make sure that the algorithm name processing - * is functional on the three embedded JOSE classes. - * - * @author jricher, tsitkov - * - */ -public class JOSEEmbedTest { - - @Test - public void testJWSAlgorithmEmbed() { - JWSAlgorithmEmbed a = new JWSAlgorithmEmbed(JWSAlgorithm.HS256); - - assertEquals(JWSAlgorithm.HS256, a.getAlgorithm()); - assertEquals("HS256", a.getAlgorithmName()); - - a.setAlgorithm(JWSAlgorithm.HS384); - assertEquals(JWSAlgorithm.HS384, a.getAlgorithm()); - - JWSAlgorithmEmbed null_a = new JWSAlgorithmEmbed(null); - assertEquals(null, null_a.getAlgorithm()); - assertEquals(null, null_a.getAlgorithmName()); - } - - @Test - public void testJWSAlgorithmEmbedGetForAlgoirthmName() { - JWSAlgorithmEmbed a = JWSAlgorithmEmbed.getForAlgorithmName("RS256"); - - assertEquals(JWSAlgorithm.RS256, a.getAlgorithm()); - assertEquals("RS256", a.getAlgorithmName()); - - JWSAlgorithmEmbed null_a = JWSAlgorithmEmbed.getForAlgorithmName(""); - assertEquals(null, null_a); - } - - @Test - public void testJWEAlgorithmEmbed() { - JWEAlgorithmEmbed a = new JWEAlgorithmEmbed(JWEAlgorithm.A128KW); - - assertEquals(JWEAlgorithm.A128KW, a.getAlgorithm()); - assertEquals("A128KW", a.getAlgorithmName()); - - a.setAlgorithm(JWEAlgorithm.A256KW); - assertEquals(JWEAlgorithm.A256KW, a.getAlgorithm()); - - JWEAlgorithmEmbed null_a = new JWEAlgorithmEmbed(null); - assertEquals(null, null_a.getAlgorithm()); - assertEquals(null, null_a.getAlgorithmName()); - } - - @Test - public void testJWEAlgorithmEmbedGetForAlgoirthmName() { - JWEAlgorithmEmbed a = JWEAlgorithmEmbed.getForAlgorithmName("RSA1_5"); - - assertEquals(JWEAlgorithm.RSA1_5, a.getAlgorithm()); - assertEquals("RSA1_5", a.getAlgorithmName()); - - JWEAlgorithmEmbed null_a = JWEAlgorithmEmbed.getForAlgorithmName(""); - assertEquals(null, null_a); - } - - @Test - public void testJWEEncryptionMethodEmbed() { - JWEEncryptionMethodEmbed a = new JWEEncryptionMethodEmbed(EncryptionMethod.A128CBC_HS256); - - assertEquals(EncryptionMethod.A128CBC_HS256, a.getAlgorithm()); - assertEquals("A128CBC-HS256", a.getAlgorithmName()); - - a.setAlgorithm(EncryptionMethod.A256GCM); - assertEquals(EncryptionMethod.A256GCM, a.getAlgorithm()); - - JWEEncryptionMethodEmbed null_a = new JWEEncryptionMethodEmbed(null); - assertEquals(null, null_a.getAlgorithm()); - assertEquals(null, null_a.getAlgorithmName()); - } - - @Test - public void testJWEEncryptionMethodEmbedGetForAlgoirthmName() { - JWEEncryptionMethodEmbed a = JWEEncryptionMethodEmbed.getForAlgorithmName("A256GCM"); - - assertEquals(EncryptionMethod.A256GCM, a.getAlgorithm()); - assertEquals("A256GCM", a.getAlgorithmName()); - - JWEEncryptionMethodEmbed null_a = JWEEncryptionMethodEmbed.getForAlgorithmName(""); - assertEquals(null, null_a); - } - -} diff --git a/openid-connect-common/src/test/java/org/mitre/jose/TestJWKSetKeyStore.java b/openid-connect-common/src/test/java/org/mitre/jose/TestJWKSetKeyStore.java index 524b4a75e3..870e1bb3be 100644 --- a/openid-connect-common/src/test/java/org/mitre/jose/TestJWKSetKeyStore.java +++ b/openid-connect-common/src/test/java/org/mitre/jose/TestJWKSetKeyStore.java @@ -1,24 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * * 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 org.mitre.jose; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -39,22 +35,25 @@ import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.util.Base64URL; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * @author tsitkov * */ -public class TestJWKSetKeyStore { +public class TestJWKSetKeyStore { private String RSAkid = "rsa_1"; private JWK RSAjwk = new RSAKey( new Base64URL("oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + - "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + - "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + - "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + - "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + - "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n new Base64URL("AQAB"), // e new Base64URL("kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + @@ -62,16 +61,16 @@ public class TestJWKSetKeyStore { "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ"), // d - KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA_OAEP, RSAkid, null, null, null); + KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA_OAEP, RSAkid, null, null, null, null, null); private String RSAkid_rsa2 = "rsa_2"; private JWK RSAjwk_rsa2 = new RSAKey( new Base64URL("oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + - "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + - "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + - "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + - "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + - "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n new Base64URL("AQAB"), // e new Base64URL("kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + @@ -79,31 +78,31 @@ public class TestJWKSetKeyStore { "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ"), // d - KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA1_5, RSAkid_rsa2, null, null, null); - - - List keys_list = new LinkedList(); - private JWKSet jwkSet; - private String ks_file = "ks.txt"; - private String ks_file_badJWK = "ks_badJWK.txt"; - + KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA1_5, RSAkid_rsa2, null, null, null, null, null); + + + List keys_list = new LinkedList<>(); + private JWKSet jwkSet; + private String ks_file = "ks.txt"; + private String ks_file_badJWK = "ks_badJWK.txt"; + @Before public void prepare() throws IOException { - + keys_list.add(RSAjwk); keys_list.add(RSAjwk_rsa2); jwkSet = new JWKSet(keys_list); jwkSet.getKeys(); - - byte jwtbyte[] = jwkSet.toString().getBytes(); + + byte jwtbyte[] = jwkSet.toString().getBytes(); FileOutputStream out = new FileOutputStream(ks_file); out.write(jwtbyte); out.close(); } - - @After - public void cleanup() throws IOException { - + + @After + public void cleanup() throws IOException { + File f1 = new File(ks_file); if (f1.exists()) { f1.delete(); @@ -112,21 +111,21 @@ public void cleanup() throws IOException { if (f2.exists()) { f2.delete(); } - } - + } + /* Constructors with no valid Resource setup */ @Test public void ksConstructorTest() { JWKSetKeyStore ks = new JWKSetKeyStore(jwkSet); assertEquals(ks.getJwkSet(), jwkSet); - + JWKSetKeyStore ks_empty= new JWKSetKeyStore(); - assertEquals(ks_empty.getJwkSet(), null); - + assertEquals(ks_empty.getJwkSet(), null); + boolean thrown = false; try { - JWKSetKeyStore ks_null = new JWKSetKeyStore(null); + new JWKSetKeyStore(null); } catch (IllegalArgumentException e) { thrown = true; } @@ -136,74 +135,43 @@ public void ksConstructorTest() { /* Misformatted JWK */ @Test(expected=IllegalArgumentException.class) public void ksBadJWKinput() throws IOException { - - byte jwtbyte[] = RSAjwk.toString().getBytes(); + + byte jwtbyte[] = RSAjwk.toString().getBytes(); FileOutputStream out = new FileOutputStream(ks_file_badJWK); out.write(jwtbyte); out.close(); - + JWKSetKeyStore ks_badJWK = new JWKSetKeyStore(); Resource loc = new FileSystemResource(ks_file_badJWK); assertTrue(loc.exists()); - + ks_badJWK.setLocation(loc); assertEquals(loc.getFilename(), ks_file_badJWK); - + ks_badJWK = new JWKSetKeyStore(null); } - + /* Empty constructor with valid Resource */ @Test public void ksEmptyConstructorkLoc() { JWKSetKeyStore ks = new JWKSetKeyStore(); - + File file = new File(ks_file); - - /* First, test with file without "read" permission */ - - boolean set = false; - - if (file.exists()) { - set = file.setReadable(false); - } - - // skip this part of the test on systems that don't allow the settable function, like Windows - if (set) { - - Resource loc_noread = new FileSystemResource(file); - assertTrue(loc_noread.exists()); - // assertTrue(!loc_noread.isReadable()); - - boolean thrown = false; - try { - ks.setLocation(loc_noread); - } catch (IllegalArgumentException e) { - thrown = true; - } - assertTrue(thrown); - - /* Now, make cache file readable */ - - if (file.exists()) { - file.setReadable(true); - } - - } - - Resource loc = new FileSystemResource(file); + + Resource loc = new FileSystemResource(file); assertTrue(loc.exists()); assertTrue(loc.isReadable()); ks.setLocation(loc); - - assertEquals(loc.getFilename(),ks.getLocation().getFilename()); + + assertEquals(loc.getFilename(),ks.getLocation().getFilename()); } - + @Test public void ksSetJwkSet() throws IllegalArgumentException { - + JWKSetKeyStore ks = new JWKSetKeyStore(); boolean thrown = false; try { diff --git a/openid-connect-common/src/test/java/org/mitre/jwt/encryption/service/impl/TestDefaultJwtEncryptionAndDecryptionService.java b/openid-connect-common/src/test/java/org/mitre/jwt/encryption/service/impl/TestDefaultJWTEncryptionAndDecryptionService.java similarity index 67% rename from openid-connect-common/src/test/java/org/mitre/jwt/encryption/service/impl/TestDefaultJwtEncryptionAndDecryptionService.java rename to openid-connect-common/src/test/java/org/mitre/jwt/encryption/service/impl/TestDefaultJWTEncryptionAndDecryptionService.java index 642be755b0..18134b411f 100644 --- a/openid-connect-common/src/test/java/org/mitre/jwt/encryption/service/impl/TestDefaultJwtEncryptionAndDecryptionService.java +++ b/openid-connect-common/src/test/java/org/mitre/jwt/encryption/service/impl/TestDefaultJWTEncryptionAndDecryptionService.java @@ -1,26 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.jwt.encryption.service.impl; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.text.ParseException; @@ -28,9 +24,16 @@ import java.util.List; import java.util.Map; +import javax.crypto.Cipher; + +import org.junit.Assume; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.mitre.jose.keystore.JWKSetKeyStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableMap; import com.nimbusds.jose.EncryptionMethod; @@ -38,6 +41,7 @@ import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWEHeader; import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.jca.JCASupport; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.KeyUse; @@ -47,7 +51,12 @@ import com.nimbusds.jose.util.JSONObjectUtils; import com.nimbusds.jwt.EncryptedJWT; import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; + +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; /** @@ -56,15 +65,20 @@ * */ -public class TestDefaultJwtEncryptionAndDecryptionService { +public class TestDefaultJWTEncryptionAndDecryptionService { + + private static Logger logger = LoggerFactory.getLogger(TestDefaultJWTEncryptionAndDecryptionService.class); private String plainText = "The true sign of intelligence is not knowledge but imagination."; private String issuer = "www.example.net"; private String subject = "example_user"; - private JWTClaimsSet claimsSet = new JWTClaimsSet(); + private JWTClaimsSet claimsSet = null; + + @Rule + public ExpectedException exception = ExpectedException.none(); - // Example data taken from Mike Jones's draft-ietf-jose-json-web-encryption-14 appendix examples + // Example data taken from rfc7516 appendix A private String compactSerializedJwe = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ." + "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe" + "ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb" + @@ -78,12 +92,13 @@ public class TestDefaultJwtEncryptionAndDecryptionService { "XFBoMYUZodetZdvTiFvSkQ"; private String RSAkid = "rsa321"; - private JWK RSAjwk = new RSAKey(new Base64URL("oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + - "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + - "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + - "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + - "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + - "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n + private JWK RSAjwk = new RSAKey( + new Base64URL("oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n new Base64URL("AQAB"), // e new Base64URL("kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + @@ -91,16 +106,16 @@ public class TestDefaultJwtEncryptionAndDecryptionService { "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ"), // d - KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA_OAEP, RSAkid, null, null, null); + KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA_OAEP, RSAkid, null, null, null, null, null); private String RSAkid_2 = "rsa3210"; private JWK RSAjwk_2 = new RSAKey( new Base64URL("oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + - "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + - "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + - "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + - "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + - "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw"), // n new Base64URL("AQAB"), // e new Base64URL("kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + @@ -108,14 +123,14 @@ public class TestDefaultJwtEncryptionAndDecryptionService { "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ"), // d - KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA1_5, RSAkid_2, null, null, null); + KeyUse.ENCRYPTION, null, JWEAlgorithm.RSA1_5, RSAkid_2, null, null, null, null, null); private String AESkid = "aes123"; - private JWK AESjwk = new OctetSequenceKey( new Base64URL("GawgguFyGrWKav7AX4VKUg"), + private JWK AESjwk = new OctetSequenceKey(new Base64URL("GawgguFyGrWKav7AX4VKUg"), KeyUse.ENCRYPTION, null, JWEAlgorithm.A128KW, - AESkid, null, null, null); - - + AESkid, null, null, null, null, null); + + private Map keys = new ImmutableMap.Builder() .put(RSAkid, RSAjwk) .build(); @@ -131,42 +146,48 @@ public class TestDefaultJwtEncryptionAndDecryptionService { .put(RSAkid_2, RSAjwk_2) .put(AESkid, AESjwk) .build(); - - private List keys_list = new LinkedList(); - private DefaultJwtEncryptionAndDecryptionService service; - private DefaultJwtEncryptionAndDecryptionService service_2; - private DefaultJwtEncryptionAndDecryptionService service_3; - private DefaultJwtEncryptionAndDecryptionService service_4; - private DefaultJwtEncryptionAndDecryptionService service_ks; - + private List keys_list = new LinkedList<>(); + + private DefaultJWTEncryptionAndDecryptionService service; + private DefaultJWTEncryptionAndDecryptionService service_2; + private DefaultJWTEncryptionAndDecryptionService service_3; + private DefaultJWTEncryptionAndDecryptionService service_4; + private DefaultJWTEncryptionAndDecryptionService service_ks; + @Before public void prepare() throws NoSuchAlgorithmException, InvalidKeySpecException, JOSEException { - service = new DefaultJwtEncryptionAndDecryptionService(keys); - service_2 = new DefaultJwtEncryptionAndDecryptionService(keys_2); - service_3 = new DefaultJwtEncryptionAndDecryptionService(keys_3); - service_4 = new DefaultJwtEncryptionAndDecryptionService(keys_4); + service = new DefaultJWTEncryptionAndDecryptionService(keys); + service_2 = new DefaultJWTEncryptionAndDecryptionService(keys_2); + service_3 = new DefaultJWTEncryptionAndDecryptionService(keys_3); + service_4 = new DefaultJWTEncryptionAndDecryptionService(keys_4); + + claimsSet = new JWTClaimsSet.Builder() + .issuer(issuer) + .subject(subject) + .build(); - claimsSet.setIssuer(issuer); - claimsSet.setSubject(subject); - // Key Store - + keys_list.add(RSAjwk); keys_list.add(AESjwk); JWKSet jwkSet = new JWKSet(keys_list); JWKSetKeyStore keyStore = new JWKSetKeyStore(jwkSet); - - service_ks = new DefaultJwtEncryptionAndDecryptionService(keyStore); + + service_ks = new DefaultJWTEncryptionAndDecryptionService(keyStore); } @Test - public void decrypt_RSA() throws ParseException { - + public void decrypt_RSA() throws ParseException, NoSuchAlgorithmException { + + Assume.assumeTrue(JCASupport.isSupported(JWEAlgorithm.RSA_OAEP) // check for algorithm support + && JCASupport.isSupported(EncryptionMethod.A256GCM) + && Cipher.getMaxAllowedKeyLength("RC5") >= 256); // check for unlimited crypto strength + service.setDefaultDecryptionKeyId(RSAkid); service.setDefaultEncryptionKeyId(RSAkid); @@ -182,7 +203,11 @@ public void decrypt_RSA() throws ParseException { @Test - public void encryptThenDecrypt_RSA() throws ParseException { + public void encryptThenDecrypt_RSA() throws ParseException, NoSuchAlgorithmException { + + Assume.assumeTrue(JCASupport.isSupported(JWEAlgorithm.RSA_OAEP) // check for algorithm support + && JCASupport.isSupported(EncryptionMethod.A256GCM) + && Cipher.getMaxAllowedKeyLength("RC5") >= 256); // check for unlimited crypto strength service.setDefaultDecryptionKeyId(RSAkid); service.setDefaultEncryptionKeyId(RSAkid); @@ -201,7 +226,7 @@ public void encryptThenDecrypt_RSA() throws ParseException { assertThat(encryptedJwt.getJWTClaimsSet(), nullValue()); service.decryptJwt(encryptedJwt); - ReadOnlyJWTClaimsSet resultClaims = encryptedJwt.getJWTClaimsSet(); + JWTClaimsSet resultClaims = encryptedJwt.getJWTClaimsSet(); assertEquals(claimsSet.getIssuer(), resultClaims.getIssuer()); assertEquals(claimsSet.getSubject(), resultClaims.getSubject()); @@ -210,7 +235,11 @@ public void encryptThenDecrypt_RSA() throws ParseException { // The same as encryptThenDecrypt_RSA() but relies on the key from the map @Test - public void encryptThenDecrypt_nullID() throws ParseException { + public void encryptThenDecrypt_nullID() throws ParseException, NoSuchAlgorithmException { + + Assume.assumeTrue(JCASupport.isSupported(JWEAlgorithm.RSA_OAEP) // check for algorithm support + && JCASupport.isSupported(EncryptionMethod.A256GCM) + && Cipher.getMaxAllowedKeyLength("RC5") >= 256); // check for unlimited crypto strength service.setDefaultDecryptionKeyId(null); service.setDefaultEncryptionKeyId(null); @@ -229,15 +258,21 @@ public void encryptThenDecrypt_nullID() throws ParseException { assertThat(encryptedJwt.getJWTClaimsSet(), nullValue()); service.decryptJwt(encryptedJwt); - ReadOnlyJWTClaimsSet resultClaims = encryptedJwt.getJWTClaimsSet(); + JWTClaimsSet resultClaims = encryptedJwt.getJWTClaimsSet(); assertEquals(claimsSet.getIssuer(), resultClaims.getIssuer()); assertEquals(claimsSet.getSubject(), resultClaims.getSubject()); } - @Test(expected=IllegalStateException.class) - public void encrypt_nullID_oneKey() { + @Test + public void encrypt_nullID_oneKey() throws NoSuchAlgorithmException { + + Assume.assumeTrue(JCASupport.isSupported(JWEAlgorithm.RSA_OAEP) // check for algorithm support + && JCASupport.isSupported(EncryptionMethod.A256GCM) + && Cipher.getMaxAllowedKeyLength("RC5") >= 256); // check for unlimited crypto strength + + exception.expect(IllegalStateException.class); service_2.setDefaultEncryptionKeyId(null); assertEquals(null, service_2.getDefaultEncryptionKeyId()); @@ -250,9 +285,16 @@ public void encrypt_nullID_oneKey() { assertEquals(null, service_2.getDefaultEncryptionKeyId()); } - - @Test(expected=IllegalStateException.class) - public void decrypt_nullID() throws ParseException { + + @Test + public void decrypt_nullID() throws ParseException, NoSuchAlgorithmException { + + Assume.assumeTrue(JCASupport.isSupported(JWEAlgorithm.RSA_OAEP) // check for algorithm support + && JCASupport.isSupported(EncryptionMethod.A256GCM) + && Cipher.getMaxAllowedKeyLength("RC5") >= 256); // check for unlimited crypto strength + + + exception.expect(IllegalStateException.class); service_2.setDefaultEncryptionKeyId(RSAkid); service_2.setDefaultDecryptionKeyId(null); @@ -286,25 +328,25 @@ public void setThenGetDefAlg() throws ParseException { @Test - public void getAllPubKeys() throws ParseException { + public void getAllPubKeys() throws ParseException { Map keys2check = service_2.getAllPublicKeys(); assertEquals( - JSONObjectUtils.getString(RSAjwk.toPublicJWK().toJSONObject(), "e"), - JSONObjectUtils.getString(keys2check.get(RSAkid).toJSONObject(), "e") - ); + JSONObjectUtils.getString(RSAjwk.toPublicJWK().toJSONObject(), "e"), + JSONObjectUtils.getString(keys2check.get(RSAkid).toJSONObject(), "e") + ); assertEquals( - JSONObjectUtils.getString(RSAjwk_2.toPublicJWK().toJSONObject(), "e"), - JSONObjectUtils.getString(keys2check.get(RSAkid_2).toJSONObject(), "e") - ); + JSONObjectUtils.getString(RSAjwk_2.toPublicJWK().toJSONObject(), "e"), + JSONObjectUtils.getString(keys2check.get(RSAkid_2).toJSONObject(), "e") + ); assertTrue(service_3.getAllPublicKeys().isEmpty()); } - - + + @Test - public void getAllCryptoAlgsSupported() throws ParseException { - + public void getAllCryptoAlgsSupported() throws ParseException { + assertTrue(service_4.getAllEncryptionAlgsSupported().contains(JWEAlgorithm.RSA_OAEP)); assertTrue(service_4.getAllEncryptionAlgsSupported().contains(JWEAlgorithm.RSA1_5)); assertTrue(service_4.getAllEncryptionAlgsSupported().contains(JWEAlgorithm.DIR)); @@ -323,24 +365,24 @@ public void getAllCryptoAlgsSupported() throws ParseException { assertTrue(service_ks.getAllEncryptionEncsSupported().contains(EncryptionMethod.A192CBC_HS384)); assertTrue(service_ks.getAllEncryptionEncsSupported().contains(EncryptionMethod.A192GCM)); assertTrue(service_ks.getAllEncryptionEncsSupported().contains(EncryptionMethod.A256GCM)); - assertTrue(service_ks.getAllEncryptionEncsSupported().contains(EncryptionMethod.A256CBC_HS512)); + assertTrue(service_ks.getAllEncryptionEncsSupported().contains(EncryptionMethod.A256CBC_HS512)); } - - + + @Test public void getDefaultCryptoKeyId() throws ParseException { - + // Test set/getDefaultEn/DecryptionKeyId assertEquals(null, service_4.getDefaultEncryptionKeyId()); - assertEquals(null, service_4.getDefaultDecryptionKeyId()); + assertEquals(null, service_4.getDefaultDecryptionKeyId()); service_4.setDefaultEncryptionKeyId(RSAkid); service_4.setDefaultDecryptionKeyId(AESkid); assertEquals(RSAkid, service_4.getDefaultEncryptionKeyId()); assertEquals(AESkid, service_4.getDefaultDecryptionKeyId()); - + assertEquals(null, service_ks.getDefaultEncryptionKeyId()); - assertEquals(null, service_ks.getDefaultDecryptionKeyId()); + assertEquals(null, service_ks.getDefaultDecryptionKeyId()); service_ks.setDefaultEncryptionKeyId(RSAkid); service_ks.setDefaultDecryptionKeyId(AESkid); assertEquals( RSAkid, service_ks.getDefaultEncryptionKeyId()) ; diff --git a/openid-connect-common/src/test/java/org/mitre/oauth2/model/ClientDetailsEntityTest.java b/openid-connect-common/src/test/java/org/mitre/oauth2/model/ClientDetailsEntityTest.java index 38db8f2585..cfcd29d9fc 100644 --- a/openid-connect-common/src/test/java/org/mitre/oauth2/model/ClientDetailsEntityTest.java +++ b/openid-connect-common/src/test/java/org/mitre/oauth2/model/ClientDetailsEntityTest.java @@ -1,26 +1,25 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; -import static org.junit.Assert.assertEquals; - import java.util.Date; import org.junit.Test; @@ -29,6 +28,8 @@ import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; +import static org.junit.Assert.assertEquals; + /** * @author jricher * diff --git a/openid-connect-common/src/test/java/org/mitre/oauth2/model/RegisteredClientTest.java b/openid-connect-common/src/test/java/org/mitre/oauth2/model/RegisteredClientTest.java index 0eaaecc20a..d973fc020e 100644 --- a/openid-connect-common/src/test/java/org/mitre/oauth2/model/RegisteredClientTest.java +++ b/openid-connect-common/src/test/java/org/mitre/oauth2/model/RegisteredClientTest.java @@ -1,26 +1,25 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.model; -import static org.junit.Assert.assertEquals; - import java.sql.Date; import org.junit.Test; @@ -29,6 +28,8 @@ import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; +import static org.junit.Assert.assertEquals; + /** * @author jricher * diff --git a/openid-connect-common/src/test/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessorTest.java b/openid-connect-common/src/test/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessorTest.java index c922544f38..ab19f8b0ab 100644 --- a/openid-connect-common/src/test/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessorTest.java +++ b/openid-connect-common/src/test/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessorTest.java @@ -1,27 +1,25 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.sql.Date; import org.junit.Test; @@ -34,6 +32,9 @@ import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * @author jricher * diff --git a/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ConfigurationPropertiesBeanTest.java b/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ConfigurationPropertiesBeanTest.java index 261cd01d8e..639b295d1d 100644 --- a/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ConfigurationPropertiesBeanTest.java +++ b/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ConfigurationPropertiesBeanTest.java @@ -1,27 +1,30 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.config; -import static org.junit.Assert.assertEquals; - import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; /** * @author jricher @@ -45,11 +48,101 @@ public void testConfigurationPropertiesBean() { bean.setIssuer(iss); bean.setTopbarTitle(title); bean.setLogoImageUrl(logoUrl); + bean.setForceHttps(true); assertEquals(iss, bean.getIssuer()); assertEquals(title, bean.getTopbarTitle()); assertEquals(logoUrl, bean.getLogoImageUrl()); + assertEquals(true, bean.isForceHttps()); + } + + @Test + public void testCheckForHttpsIssuerHttpDefaultFlag() { + ConfigurationPropertiesBean bean = new ConfigurationPropertiesBean(); + + // issuer is http + // leave as default, which is unset/false + try { + bean.setIssuer("http://localhost:8080/openid-connect-server/"); + bean.checkConfigConsistency(); + } catch (BeanCreationException e) { + fail("Unexpected BeanCreationException for http issuer with default forceHttps, message:" + e.getMessage()); + } + } + + @Test + public void testCheckForHttpsIssuerHttpFalseFlag() { + ConfigurationPropertiesBean bean = new ConfigurationPropertiesBean(); + // issuer is http + // set to false + try { + bean.setIssuer("http://localhost:8080/openid-connect-server/"); + bean.setForceHttps(false); + bean.checkConfigConsistency(); + } catch (BeanCreationException e) { + fail("Unexpected BeanCreationException for http issuer with forceHttps=false, message:" + e.getMessage()); + } + } + @Test(expected = BeanCreationException.class) + public void testCheckForHttpsIssuerHttpTrueFlag() { + ConfigurationPropertiesBean bean = new ConfigurationPropertiesBean(); + // issuer is http + // set to true + bean.setIssuer("http://localhost:8080/openid-connect-server/"); + bean.setForceHttps(true); + bean.checkConfigConsistency(); + } + + @Test + public void testCheckForHttpsIssuerHttpsDefaultFlag() { + ConfigurationPropertiesBean bean = new ConfigurationPropertiesBean(); + // issuer is https + // leave as default, which is unset/false + try { + bean.setIssuer("https://localhost:8080/openid-connect-server/"); + bean.checkConfigConsistency(); + } catch (BeanCreationException e) { + fail("Unexpected BeanCreationException for https issuer with default forceHttps, message:" + e.getMessage()); + } + } + + @Test + public void testCheckForHttpsIssuerHttpsFalseFlag() { + ConfigurationPropertiesBean bean = new ConfigurationPropertiesBean(); + // issuer is https + // set to false + try { + bean.setIssuer("https://localhost:8080/openid-connect-server/"); + bean.setForceHttps(false); + bean.checkConfigConsistency(); + } catch (BeanCreationException e) { + fail("Unexpected BeanCreationException for https issuer with forceHttps=false, message:" + e.getMessage()); + } + } + + @Test + public void testCheckForHttpsIssuerHttpsTrueFlag() { + ConfigurationPropertiesBean bean = new ConfigurationPropertiesBean(); + // issuer is https + // set to true + try { + bean.setIssuer("https://localhost:8080/openid-connect-server/"); + bean.setForceHttps(true); + bean.checkConfigConsistency(); + } catch (BeanCreationException e) { + fail("Unexpected BeanCreationException for https issuer with forceHttps=true, message:" + e.getMessage()); + } + + } + + @Test + public void testShortTopbarTitle() { + ConfigurationPropertiesBean bean = new ConfigurationPropertiesBean(); + bean.setTopbarTitle("LONG"); + assertEquals("LONG", bean.getShortTopbarTitle()); + bean.setShortTopbarTitle("SHORT"); + assertEquals("SHORT", bean.getShortTopbarTitle()); } } diff --git a/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ServerConfigurationTest.java b/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ServerConfigurationTest.java index 85f362a9a1..7e7513e087 100644 --- a/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ServerConfigurationTest.java +++ b/openid-connect-common/src/test/java/org/mitre/openid/connect/config/ServerConfigurationTest.java @@ -1,29 +1,30 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.config; +import org.junit.Test; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import org.junit.Test; - /** * @author jricher * diff --git a/openid-connect-server-webapp/pom.xml b/openid-connect-server-webapp/pom.xml index da50278f0d..a294b1d8bb 100644 --- a/openid-connect-server-webapp/pom.xml +++ b/openid-connect-server-webapp/pom.xml @@ -1,153 +1,152 @@ + - 4.0.0 - - org.mitre - openid-connect-parent - 1.1.10-SNAPSHOT - - openid-connect-server-webapp - war - OpenID Connect Server Webapp - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java-version} - ${java-version} - - - - org.apache.maven.plugins - maven-war-plugin - - openid-connect-server-webapp - - gif - ico - jpg - png - pdf - + 4.0.0 + + org.mitre + openid-connect-parent + 1.3.5-SNAPSHOT + + openid-connect-server-webapp + war + OpenID Connect Server Webapp + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java-version} + ${java-version} + + + + org.apache.maven.plugins + maven-war-plugin + + openid-connect-server-webapp src/main/webapp true + + **/*.tag + **/*.jsp + + + + src/main/webapp + false + + **/*.tag + **/*.jsp + - - - - org.apache.maven.plugins - maven-dependency-plugin - - - install - install - - sources - - - - - - org.eclipse.jetty - jetty-maven-plugin - - - /openid-connect-server-webapp - - - - - - - - org.mitre - openid-connect-server - 1.1.10-SNAPSHOT - - - org.springframework - spring-orm - ${org.springframework-version} - - - - commons-logging - commons-logging - - - - - org.slf4j - jcl-over-slf4j - ${org.slf4j-version} - runtime - - - org.slf4j - slf4j-log4j12 - ${org.slf4j-version} - runtime - - - log4j - log4j - 1.2.15 - - - javax.mail - mail - - - javax.jms - jms - - - com.sun.jdmk - jmxtools - - - com.sun.jmx - jmxri - - - runtime - - - commons-dbcp - commons-dbcp - 1.4 - + less/** + + + + org.apache.maven.plugins + maven-dependency-plugin + + + install + install + + sources + + + + + + org.eclipse.jetty + jetty-maven-plugin + + ${project.build.directory}/openid-connect-server-webapp.war + + /openid-connect-server-webapp + + + + + ro.isdc.wro4j + wro4j-maven-plugin + + bootstrap,bootstrap-responsive + ${project.build.directory}/${project.build.finalName} + ${project.build.directory}/${project.build.finalName}/resources/bootstrap2/css/ + ${project.build.directory}/${project.build.finalName}/js/ + ro.isdc.wro.maven.plugin.manager.factory.ConfigurableWroManagerFactory + + + + + + + + org.mitre + openid-connect-server + + + org.springframework + spring-orm + + + commons-logging + commons-logging + + + + + org.slf4j + jcl-over-slf4j + + + org.slf4j + slf4j-log4j12 + + + log4j + log4j + org.hsqldb hsqldb - 2.2.9 - - org.eclipse.persistence - org.eclipse.persistence.jpa - 2.5.1 - - - org.springframework.security - spring-security-taglibs - ${spring.security.version} - - - org.springframework - * - - - - - javax.servlet - jstl - 1.2 - + + org.eclipse.persistence + org.eclipse.persistence.jpa + + + org.springframework.security + spring-security-taglibs + + + javax.servlet + jstl + - + + com.zaxxer + HikariCP + + + Deployable package of the OpenID Connect server diff --git a/openid-connect-server-webapp/src/main/resources/db/clients.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/clients.sql similarity index 98% rename from openid-connect-server-webapp/src/main/resources/db/clients.sql rename to openid-connect-server-webapp/src/main/resources/db/hsql/clients.sql index 7e7a8a53d9..1410f7bd15 100644 --- a/openid-connect-server-webapp/src/main/resources/db/clients.sql +++ b/openid-connect-server-webapp/src/main/resources/db/hsql/clients.sql @@ -28,6 +28,7 @@ INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'authorization_code'), ('client', 'urn:ietf:params:oauth:grant_type:redelegate'), + ('client', 'urn:ietf:params:oauth:grant-type:device_code'), ('client', 'implicit'), ('client', 'refresh_token'); diff --git a/openid-connect-server-webapp/src/main/resources/db/hsql/hsql_database_index.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/hsql_database_index.sql new file mode 100644 index 0000000000..38636a96f9 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/hsql/hsql_database_index.sql @@ -0,0 +1,19 @@ +-- +-- Indexes for HSQLDB +-- + +CREATE INDEX IF NOT EXISTS at_tv_idx ON access_token(token_value); +CREATE INDEX IF NOT EXISTS ts_oi_idx ON token_scope(owner_id); +CREATE INDEX IF NOT EXISTS at_exp_idx ON access_token(expiration); +CREATE INDEX IF NOT EXISTS rf_ahi_idx ON refresh_token(auth_holder_id); +CREATE INDEX IF NOT EXISTS rf_tv_idx ON refresh_token(token_value); +CREATE INDEX IF NOT EXISTS cd_ci_idx ON client_details(client_id); +CREATE INDEX IF NOT EXISTS at_ahi_idx ON access_token(auth_holder_id); +CREATE INDEX IF NOT EXISTS aha_oi_idx ON authentication_holder_authority(owner_id); +CREATE INDEX IF NOT EXISTS ahe_oi_idx ON authentication_holder_extension(owner_id); +CREATE INDEX IF NOT EXISTS ahrp_oi_idx ON authentication_holder_request_parameter(owner_id); +CREATE INDEX IF NOT EXISTS ahri_oi_idx ON authentication_holder_resource_id(owner_id); +CREATE INDEX IF NOT EXISTS ahrt_oi_idx ON authentication_holder_response_type(owner_id); +CREATE INDEX IF NOT EXISTS ahs_oi_idx ON authentication_holder_scope(owner_id); +CREATE INDEX IF NOT EXISTS ac_ahi_idx ON authorization_code(auth_holder_id); +CREATE INDEX IF NOT EXISTS suaa_oi_idx ON saved_user_auth_authority(owner_id); diff --git a/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql similarity index 53% rename from openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql rename to openid-connect-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql index fe3cf32c0b..2a01756298 100644 --- a/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql +++ b/openid-connect-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql @@ -8,10 +8,15 @@ CREATE TABLE IF NOT EXISTS access_token ( expiration TIMESTAMP, token_type VARCHAR(256), refresh_token_id BIGINT, - client_id VARCHAR(256), + client_id BIGINT, auth_holder_id BIGINT, - id_token_id BIGINT, - approved_site_id BIGINT + approved_site_id BIGINT, + UNIQUE(token_value) +); + +CREATE TABLE IF NOT EXISTS access_token_permissions ( + access_token_id BIGINT NOT NULL, + permission_id BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS address ( @@ -26,12 +31,12 @@ CREATE TABLE IF NOT EXISTS address ( CREATE TABLE IF NOT EXISTS approved_site ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, - user_id VARCHAR(4096), - client_id VARCHAR(4096), + user_id VARCHAR(256), + client_id VARCHAR(256), creation_date TIMESTAMP, access_date TIMESTAMP, timeout_date TIMESTAMP, - whitelisted_site_id VARCHAR(256) + whitelisted_site_id BIGINT ); CREATE TABLE IF NOT EXISTS approved_site_scope ( @@ -41,19 +46,66 @@ CREATE TABLE IF NOT EXISTS approved_site_scope ( CREATE TABLE IF NOT EXISTS authentication_holder ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + user_auth_id BIGINT, + approved BOOLEAN, + redirect_uri VARCHAR(2048), + client_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_authority ( + owner_id BIGINT, + authority VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_resource_id ( + owner_id BIGINT, + resource_id VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_response_type ( + owner_id BIGINT, + response_type VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_extension ( owner_id BIGINT, - authentication LONGVARBINARY + extension VARCHAR(2048), + val VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_scope ( + owner_id BIGINT, + scope VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_request_parameter ( + owner_id BIGINT, + param VARCHAR(2048), + val VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS saved_user_auth ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + name VARCHAR(1024), + authenticated BOOLEAN, + source_class VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS saved_user_auth_authority ( + owner_id BIGINT, + authority VARCHAR(256) ); CREATE TABLE IF NOT EXISTS client_authority ( owner_id BIGINT, - authority LONGVARBINARY + authority VARCHAR(256) ); CREATE TABLE IF NOT EXISTS authorization_code ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, code VARCHAR(256), - authentication LONGVARBINARY + auth_holder_id BIGINT, + expiration TIMESTAMP ); CREATE TABLE IF NOT EXISTS client_grant_type ( @@ -79,6 +131,7 @@ CREATE TABLE IF NOT EXISTS client_details ( dynamically_registered BOOLEAN DEFAULT false NOT NULL, allow_introspection BOOLEAN DEFAULT false NOT NULL, id_token_validity_seconds BIGINT DEFAULT 600 NOT NULL, + device_code_validity_seconds BIGINT, client_id VARCHAR(256), client_secret VARCHAR(2048), @@ -96,6 +149,7 @@ CREATE TABLE IF NOT EXISTS client_details ( tos_uri VARCHAR(2048), jwks_uri VARCHAR(2048), + jwks VARCHAR(8192), sector_identifier_uri VARCHAR(2048), request_object_signing_alg VARCHAR(256), @@ -114,7 +168,14 @@ CREATE TABLE IF NOT EXISTS client_details ( require_auth_time BOOLEAN, created_at TIMESTAMP, initiate_login_uri VARCHAR(2048), - post_logout_redirect_uri VARCHAR(2048), + clear_access_tokens_on_refresh BOOLEAN DEFAULT true NOT NULL, + + software_statement VARCHAR(4096), + software_id VARCHAR(2048), + software_version VARCHAR(2048), + + code_challenge_method VARCHAR(256), + UNIQUE (client_id) ); @@ -123,6 +184,11 @@ CREATE TABLE IF NOT EXISTS client_request_uri ( request_uri VARCHAR(2000) ); +CREATE TABLE IF NOT EXISTS client_post_logout_redirect_uri ( + owner_id BIGINT, + post_logout_redirect_uri VARCHAR(2000) +); + CREATE TABLE IF NOT EXISTS client_default_acr_value ( owner_id BIGINT, default_acr_value VARCHAR(2000) @@ -138,12 +204,17 @@ CREATE TABLE IF NOT EXISTS client_redirect_uri ( redirect_uri VARCHAR(2048) ); +CREATE TABLE IF NOT EXISTS client_claims_redirect_uri ( + owner_id BIGINT, + redirect_uri VARCHAR(2048) +); + CREATE TABLE IF NOT EXISTS refresh_token ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, token_value VARCHAR(4096), expiration TIMESTAMP, auth_holder_id BIGINT, - client_id VARCHAR(256) + client_id BIGINT ); CREATE TABLE IF NOT EXISTS client_resource ( @@ -166,10 +237,8 @@ CREATE TABLE IF NOT EXISTS system_scope ( scope VARCHAR(256) NOT NULL, description VARCHAR(4096), icon VARCHAR(256), - allow_dyn_reg BOOLEAN DEFAULT false NOT NULL, + restricted BOOLEAN DEFAULT false NOT NULL, default_scope BOOLEAN DEFAULT false NOT NULL, - structured BOOLEAN DEFAULT false NOT NULL, - structured_param_description VARCHAR(256), UNIQUE (scope) ); @@ -194,7 +263,8 @@ CREATE TABLE IF NOT EXISTS user_info ( phone_number_verified BOOLEAN, address_id VARCHAR(256), updated_time VARCHAR(256), - birthdate VARCHAR(256) + birthdate VARCHAR(256), + src VARCHAR(4096) ); CREATE TABLE IF NOT EXISTS whitelisted_site ( @@ -214,3 +284,101 @@ CREATE TABLE IF NOT EXISTS pairwise_identifier ( sub VARCHAR(256), sector_identifier VARCHAR(2048) ); + +CREATE TABLE IF NOT EXISTS resource_set ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + name VARCHAR(1024) NOT NULL, + uri VARCHAR(1024), + icon_uri VARCHAR(1024), + rs_type VARCHAR(256), + owner VARCHAR(256) NOT NULL, + client_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS resource_set_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS permission_ticket ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + ticket VARCHAR(256) NOT NULL, + permission_id BIGINT NOT NULL, + expiration TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS permission ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + resource_set_id BIGINT +); + +CREATE TABLE IF NOT EXISTS permission_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + name VARCHAR(256), + friendly_name VARCHAR(1024), + claim_type VARCHAR(1024), + claim_value VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS claim_to_policy ( + policy_id BIGINT NOT NULL, + claim_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim_to_permission_ticket ( + permission_ticket_id BIGINT NOT NULL, + claim_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS policy ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + name VARCHAR(1024), + resource_set_id BIGINT +); + +CREATE TABLE IF NOT EXISTS policy_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim_token_format ( + owner_id BIGINT NOT NULL, + claim_token_format VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS claim_issuer ( + owner_id BIGINT NOT NULL, + issuer VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS saved_registered_client ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + issuer VARCHAR(1024), + registered_client VARCHAR(8192) +); + +CREATE TABLE IF NOT EXISTS device_code ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, + device_code VARCHAR(1024), + user_code VARCHAR(1024), + expiration TIMESTAMP, + client_id VARCHAR(256), + approved BOOLEAN, + auth_holder_id BIGINT +); + +CREATE TABLE IF NOT EXISTS device_code_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS device_code_request_parameter ( + owner_id BIGINT, + param VARCHAR(2048), + val VARCHAR(2048) +); diff --git a/openid-connect-server-webapp/src/main/resources/db/tables/loading_temp_tables.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/loading_temp_tables.sql similarity index 90% rename from openid-connect-server-webapp/src/main/resources/db/tables/loading_temp_tables.sql rename to openid-connect-server-webapp/src/main/resources/db/hsql/loading_temp_tables.sql index c1e7465306..37b0092e75 100644 --- a/openid-connect-server-webapp/src/main/resources/db/tables/loading_temp_tables.sql +++ b/openid-connect-server-webapp/src/main/resources/db/hsql/loading_temp_tables.sql @@ -14,8 +14,7 @@ CREATE TEMPORARY TABLE IF NOT EXISTS users_TEMP ( enabled boolean not null); CREATE TEMPORARY TABLE IF NOT EXISTS user_info_TEMP ( - id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, - sub VARCHAR(256), + sub VARCHAR(256) not null primary key, preferred_username VARCHAR(256), name VARCHAR(256), given_name VARCHAR(256), @@ -69,8 +68,6 @@ CREATE TEMPORARY TABLE IF NOT EXISTS system_scope_TEMP ( scope VARCHAR(256), description VARCHAR(4096), icon VARCHAR(256), - allow_dyn_reg BOOLEAN, - default_scope BOOLEAN, - structured BOOLEAN, - structured_param_description VARCHAR(256) + restricted BOOLEAN, + default_scope BOOLEAN ); \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/resources/db/hsql/scopes.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/scopes.sql new file mode 100644 index 0000000000..8e72c88c7f --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/hsql/scopes.sql @@ -0,0 +1,33 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT FALSE; + +START TRANSACTION; + +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('openid', 'log in using your identity', 'user', false, true), + ('profile', 'basic profile information', 'list-alt', false, true), + ('email', 'email address', 'envelope', false, true), + ('address', 'physical address', 'home', false, true), + ('phone', 'telephone number', 'bell', false, true), + ('offline_access', 'offline access', 'time', false, false); + +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +MERGE INTO system_scope + USING (SELECT scope, description, icon, restricted, default_scope FROM system_scope_TEMP) AS vals(scope, description, icon, restricted, default_scope) + ON vals.scope = system_scope.scope + WHEN NOT MATCHED THEN + INSERT (scope, description, icon, restricted, default_scope) VALUES(vals.scope, vals.description, vals.icon, vals.restricted, vals.default_scope); + +COMMIT; + +SET AUTOCOMMIT TRUE; \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/resources/db/tables/security-schema.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/security-schema.sql similarity index 100% rename from openid-connect-server-webapp/src/main/resources/db/tables/security-schema.sql rename to openid-connect-server-webapp/src/main/resources/db/hsql/security-schema.sql diff --git a/openid-connect-server-webapp/src/main/resources/db/users.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/users.sql similarity index 100% rename from openid-connect-server-webapp/src/main/resources/db/users.sql rename to openid-connect-server-webapp/src/main/resources/db/hsql/users.sql diff --git a/openid-connect-server-webapp/src/main/resources/db/mysql/clients.sql b/openid-connect-server-webapp/src/main/resources/db/mysql/clients.sql new file mode 100644 index 0000000000..7f02557899 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/mysql/clients.sql @@ -0,0 +1,61 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT = 0; + +START TRANSACTION; + +-- +-- Insert client information into the temporary tables. To add clients to the HSQL database, edit things here. +-- + +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('client', 'secret', 'Test Client', false, null, 3600, 600, true); + +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES + ('client', 'openid'), + ('client', 'profile'), + ('client', 'email'), + ('client', 'address'), + ('client', 'phone'), + ('client', 'offline_access'); + +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES + ('client', 'http://localhost/'), + ('client', 'http://localhost:8080/'); + +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES + ('client', 'authorization_code'), + ('client', 'urn:ietf:params:oauth:grant_type:redelegate'), + ('client', 'implicit'), + ('client', 'refresh_token'); + +-- +-- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. +-- + +INSERT INTO client_details (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) + SELECT client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection FROM client_details_TEMP + ON DUPLICATE KEY UPDATE client_details.client_id = client_details.client_id; + +INSERT INTO client_scope (owner_id, scope) + SELECT id, scope FROM client_scope_TEMP, client_details WHERE client_details.client_id = client_scope_TEMP.owner_id + ON DUPLICATE KEY UPDATE client_scope.owner_id = client_scope.owner_id; + +INSERT INTO client_redirect_uri (owner_id, redirect_uri) + SELECT id, redirect_uri FROM client_redirect_uri_TEMP, client_details WHERE client_details.client_id = client_redirect_uri_TEMP.owner_id + ON DUPLICATE KEY UPDATE client_redirect_uri.owner_id = client_redirect_uri.owner_id; + +INSERT INTO client_grant_type (owner_id, grant_type) + SELECT id, grant_type FROM client_grant_type_TEMP, client_details WHERE client_details.client_id = client_grant_type_TEMP.owner_id + ON DUPLICATE KEY UPDATE client_grant_type.owner_id = client_grant_type.owner_id; + +-- +-- Close the transaction and turn autocommit back on +-- + +COMMIT; + +SET AUTOCOMMIT = 1; + diff --git a/openid-connect-server-webapp/src/main/resources/db/mysql/mysql_database_index.sql b/openid-connect-server-webapp/src/main/resources/db/mysql/mysql_database_index.sql new file mode 100644 index 0000000000..f5daf991da --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/mysql/mysql_database_index.sql @@ -0,0 +1,19 @@ +-- +-- Indexes for MySQL +-- + +CREATE INDEX at_tv_idx ON access_token(token_value(767)); +CREATE INDEX ts_oi_idx ON token_scope(owner_id); +CREATE INDEX at_exp_idx ON access_token(expiration); +CREATE INDEX rf_ahi_idx ON refresh_token(auth_holder_id); +CREATE INDEX rf_tv_idx ON refresh_token(token_value(105)); +CREATE INDEX cd_ci_idx ON client_details(client_id); +CREATE INDEX at_ahi_idx ON access_token(auth_holder_id); +CREATE INDEX aha_oi_idx ON authentication_holder_authority(owner_id); +CREATE INDEX ahe_oi_idx ON authentication_holder_extension(owner_id); +CREATE INDEX ahrp_oi_idx ON authentication_holder_request_parameter(owner_id); +CREATE INDEX ahri_oi_idx ON authentication_holder_resource_id(owner_id); +CREATE INDEX ahrt_oi_idx ON authentication_holder_response_type(owner_id); +CREATE INDEX ahs_oi_idx ON authentication_holder_scope(owner_id); +CREATE INDEX ac_ahi_idx ON authorization_code(auth_holder_id); +CREATE INDEX suaa_oi_idx ON saved_user_auth_authority(owner_id); diff --git a/openid-connect-server-webapp/src/main/resources/db/tables/mysql_database_tables.sql b/openid-connect-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql similarity index 50% rename from openid-connect-server-webapp/src/main/resources/db/tables/mysql_database_tables.sql rename to openid-connect-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql index 2b34ed9f68..7e00cc8762 100644 --- a/openid-connect-server-webapp/src/main/resources/db/tables/mysql_database_tables.sql +++ b/openid-connect-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql @@ -8,12 +8,16 @@ CREATE TABLE IF NOT EXISTS access_token ( expiration TIMESTAMP NULL, token_type VARCHAR(256), refresh_token_id BIGINT, - client_id VARCHAR(256), + client_id BIGINT, auth_holder_id BIGINT, - id_token_id BIGINT, approved_site_id BIGINT ); +CREATE TABLE IF NOT EXISTS access_token_permissions ( + access_token_id BIGINT NOT NULL, + permission_id BIGINT NOT NULL +); + CREATE TABLE IF NOT EXISTS address ( id BIGINT AUTO_INCREMENT PRIMARY KEY, formatted VARCHAR(256), @@ -26,12 +30,12 @@ CREATE TABLE IF NOT EXISTS address ( CREATE TABLE IF NOT EXISTS approved_site ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id VARCHAR(4096), - client_id VARCHAR(4096), + user_id VARCHAR(256), + client_id VARCHAR(256), creation_date TIMESTAMP NULL, access_date TIMESTAMP NULL, timeout_date TIMESTAMP NULL, - whitelisted_site_id VARCHAR(256) + whitelisted_site_id BIGINT ); CREATE TABLE IF NOT EXISTS approved_site_scope ( @@ -41,19 +45,66 @@ CREATE TABLE IF NOT EXISTS approved_site_scope ( CREATE TABLE IF NOT EXISTS authentication_holder ( id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_auth_id BIGINT, + approved BOOLEAN, + redirect_uri VARCHAR(2048), + client_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_authority ( + owner_id BIGINT, + authority VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_resource_id ( + owner_id BIGINT, + resource_id VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_response_type ( + owner_id BIGINT, + response_type VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_extension ( + owner_id BIGINT, + extension VARCHAR(2048), + val VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_scope ( + owner_id BIGINT, + scope VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_request_parameter ( + owner_id BIGINT, + param VARCHAR(2048), + val VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS saved_user_auth ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(1024), + authenticated BOOLEAN, + source_class VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS saved_user_auth_authority ( owner_id BIGINT, - authentication LONGBLOB + authority VARCHAR(256) ); CREATE TABLE IF NOT EXISTS client_authority ( owner_id BIGINT, - authority LONGBLOB + authority VARCHAR(256) ); CREATE TABLE IF NOT EXISTS authorization_code ( id BIGINT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(256), - authentication LONGBLOB + auth_holder_id BIGINT, + expiration TIMESTAMP NULL ); CREATE TABLE IF NOT EXISTS client_grant_type ( @@ -73,12 +124,13 @@ CREATE TABLE IF NOT EXISTS blacklisted_site ( CREATE TABLE IF NOT EXISTS client_details ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - + client_description VARCHAR(1024), - reuse_refresh_tokens BOOLEAN NOT NULL DEFAULT 1, - dynamically_registered BOOLEAN NOT NULL DEFAULT 0, - allow_introspection BOOLEAN NOT NULL DEFAULT 0, - id_token_validity_seconds BIGINT NOT NULL DEFAULT 600, + reuse_refresh_tokens BOOLEAN DEFAULT true NOT NULL, + dynamically_registered BOOLEAN DEFAULT false NOT NULL, + allow_introspection BOOLEAN DEFAULT false NOT NULL, + id_token_validity_seconds BIGINT DEFAULT 600 NOT NULL, + device_code_validity_seconds BIGINT, client_id VARCHAR(256), client_secret VARCHAR(2048), @@ -96,6 +148,7 @@ CREATE TABLE IF NOT EXISTS client_details ( tos_uri VARCHAR(2048), jwks_uri VARCHAR(2048), + jwks VARCHAR(8192), sector_identifier_uri VARCHAR(2048), request_object_signing_alg VARCHAR(256), @@ -114,8 +167,15 @@ CREATE TABLE IF NOT EXISTS client_details ( require_auth_time BOOLEAN, created_at TIMESTAMP NULL, initiate_login_uri VARCHAR(2048), - post_logout_redirect_uri VARCHAR(2048), - unique(client_id) + clear_access_tokens_on_refresh BOOLEAN DEFAULT true NOT NULL, + + software_statement VARCHAR(4096), + software_id VARCHAR(2048), + software_version VARCHAR(2048), + + code_challenge_method VARCHAR(256), + + UNIQUE (client_id) ); CREATE TABLE IF NOT EXISTS client_request_uri ( @@ -123,6 +183,11 @@ CREATE TABLE IF NOT EXISTS client_request_uri ( request_uri VARCHAR(2000) ); +CREATE TABLE IF NOT EXISTS client_post_logout_redirect_uri ( + owner_id BIGINT, + post_logout_redirect_uri VARCHAR(2000) +); + CREATE TABLE IF NOT EXISTS client_default_acr_value ( owner_id BIGINT, default_acr_value VARCHAR(2000) @@ -138,12 +203,17 @@ CREATE TABLE IF NOT EXISTS client_redirect_uri ( redirect_uri VARCHAR(2048) ); +CREATE TABLE IF NOT EXISTS client_claims_redirect_uri ( + owner_id BIGINT, + redirect_uri VARCHAR(2048) +); + CREATE TABLE IF NOT EXISTS refresh_token ( id BIGINT AUTO_INCREMENT PRIMARY KEY, token_value VARCHAR(4096), expiration TIMESTAMP NULL, auth_holder_id BIGINT, - client_id VARCHAR(256) + client_id BIGINT ); CREATE TABLE IF NOT EXISTS client_resource ( @@ -166,11 +236,9 @@ CREATE TABLE IF NOT EXISTS system_scope ( scope VARCHAR(256) NOT NULL, description VARCHAR(4096), icon VARCHAR(256), - allow_dyn_reg BOOLEAN NOT NULL DEFAULT 0, - default_scope BOOLEAN NOT NULL DEFAULT 0, - structured BOOLEAN NOT NULL DEFAULT 0, - structured_param_description VARCHAR(256), - unique(scope) + restricted BOOLEAN DEFAULT false NOT NULL, + default_scope BOOLEAN DEFAULT false NOT NULL, + UNIQUE (scope) ); CREATE TABLE IF NOT EXISTS user_info ( @@ -194,7 +262,8 @@ CREATE TABLE IF NOT EXISTS user_info ( phone_number_verified BOOLEAN, address_id VARCHAR(256), updated_time VARCHAR(256), - birthdate VARCHAR(256) + birthdate VARCHAR(256), + src VARCHAR(4096) ); CREATE TABLE IF NOT EXISTS whitelisted_site ( @@ -214,3 +283,101 @@ CREATE TABLE IF NOT EXISTS pairwise_identifier ( sub VARCHAR(256), sector_identifier VARCHAR(2048) ); + +CREATE TABLE IF NOT EXISTS resource_set ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(1024) NOT NULL, + uri VARCHAR(1024), + icon_uri VARCHAR(1024), + rs_type VARCHAR(256), + owner VARCHAR(256) NOT NULL, + client_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS resource_set_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS permission_ticket ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + ticket VARCHAR(256) NOT NULL, + permission_id BIGINT NOT NULL, + expiration TIMESTAMP NULL +); + +CREATE TABLE IF NOT EXISTS permission ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + resource_set_id BIGINT +); + +CREATE TABLE IF NOT EXISTS permission_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(256), + friendly_name VARCHAR(1024), + claim_type VARCHAR(1024), + claim_value VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS claim_to_policy ( + policy_id BIGINT NOT NULL, + claim_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim_to_permission_ticket ( + permission_ticket_id BIGINT NOT NULL, + claim_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS policy ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(1024), + resource_set_id BIGINT +); + +CREATE TABLE IF NOT EXISTS policy_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim_token_format ( + owner_id BIGINT NOT NULL, + claim_token_format VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS claim_issuer ( + owner_id BIGINT NOT NULL, + issuer VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS saved_registered_client ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + issuer VARCHAR(1024), + registered_client VARCHAR(8192) +); + +CREATE TABLE IF NOT EXISTS device_code ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + device_code VARCHAR(1024), + user_code VARCHAR(1024), + expiration TIMESTAMP NULL, + client_id VARCHAR(256), + approved BOOLEAN, + auth_holder_id BIGINT +); + +CREATE TABLE IF NOT EXISTS device_code_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS device_code_request_parameter ( + owner_id BIGINT, + param VARCHAR(2048), + val VARCHAR(2048) +); diff --git a/openid-connect-server-webapp/src/main/resources/db/mysql/scopes.sql b/openid-connect-server-webapp/src/main/resources/db/mysql/scopes.sql new file mode 100644 index 0000000000..3768977ec1 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/mysql/scopes.sql @@ -0,0 +1,31 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT = 0; + +START TRANSACTION; + +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('openid', 'log in using your identity', 'user', false, true), + ('profile', 'basic profile information', 'list-alt', false, true), + ('email', 'email address', 'envelope', false, true), + ('address', 'physical address', 'home', false, true), + ('phone', 'telephone number', 'bell', false, true), + ('offline_access', 'offline access', 'time', false, false); + +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +INSERT INTO system_scope (scope, description, icon, restricted, default_scope, structured, structured_param_description) + SELECT scope, description, icon, restricted, default_scope, structured, structured_param_description FROM system_scope_TEMP + ON DUPLICATE KEY UPDATE system_scope.scope = system_scope.scope; + +COMMIT; + +SET AUTOCOMMIT = 1; diff --git a/openid-connect-server-webapp/src/main/resources/db/mysql/security-schema.sql b/openid-connect-server-webapp/src/main/resources/db/mysql/security-schema.sql new file mode 100644 index 0000000000..bc5d70b880 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/mysql/security-schema.sql @@ -0,0 +1,14 @@ +-- +-- Tables for Spring Security's user details service +-- + +create table IF NOT EXISTS users( + username varchar(50) not null primary key, + password varchar(50) not null, + enabled boolean not null); + + create table IF NOT EXISTS authorities ( + username varchar(50) not null, + authority varchar(50) not null, + constraint fk_authorities_users foreign key(username) references users(username), + constraint ix_authority unique (username,authority)); \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/resources/db/mysql/users.sql b/openid-connect-server-webapp/src/main/resources/db/mysql/users.sql new file mode 100644 index 0000000000..fc82e48006 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/mysql/users.sql @@ -0,0 +1,52 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT = 0; + +START TRANSACTION; + +-- +-- Insert user information into the temporary tables. To add users to the HSQL database, edit things here. +-- + +INSERT INTO users_TEMP (username, password, enabled) VALUES + ('admin','password',true), + ('user','password',true); + + +INSERT INTO authorities_TEMP (username, authority) VALUES + ('admin','ROLE_ADMIN'), + ('admin','ROLE_USER'), + ('user','ROLE_USER'); + +-- By default, the username column here has to match the username column in the users table, above +INSERT INTO user_info_TEMP (sub, preferred_username, name, email, email_verified) VALUES + ('90342.ASDFJWFA','admin','Demo Admin','admin@example.com', true), + ('01921.FLANRJQW','user','Demo User','user@example.com', true); + + +-- +-- Merge the temporary users safely into the database. This is a two-step process to keep users from being created on every startup with a persistent store. +-- + +INSERT INTO users (username, password, enabled) + SELECT username, password, enabled FROM users_TEMP + ON DUPLICATE KEY UPDATE users.username = users.username; + +INSERT INTO authorities (username,authority) + SELECT username, authority FROM authorities_TEMP + ON DUPLICATE KEY UPDATE authorities.username = authorities.username; + +INSERT INTO user_info (sub, preferred_username, name, email, email_verified) + SELECT sub, preferred_username, name, email, email_verified FROM user_info_TEMP + ON DUPLICATE KEY UPDATE user_info.preferred_username = user_info.preferred_username; + +-- +-- Close the transaction and turn autocommit back on +-- + +COMMIT; + +SET AUTOCOMMIT = 1; + diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/clients_oracle.sql b/openid-connect-server-webapp/src/main/resources/db/oracle/clients_oracle.sql new file mode 100644 index 0000000000..488d928457 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/clients_oracle.sql @@ -0,0 +1,51 @@ +-- +-- Insert client information into the temporary tables. To add clients to the Oracle database, edit things here. +-- + +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('client', 'secret', 'Test Client', 0, null, 3600, 600, 1); + +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'openid'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'profile'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'email'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'address'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'phone'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'offline_access'); + +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES ('client', 'http://localhost/'); +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES ('client', 'http://localhost:8080/'); + +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'authorization_code'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'urn:ietf:params:oauth:grant_type:redelegate'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'implicit'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'refresh_token'); + +-- +-- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. +-- + +MERGE INTO client_details + USING (SELECT client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection FROM client_details_TEMP) vals + ON (vals.client_id = client_details.client_id) + WHEN NOT MATCHED THEN + INSERT (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, + id_token_validity_seconds, allow_introspection) VALUES(client_details_seq.nextval, vals.client_id, vals.client_secret, vals.client_name, vals.dynamically_registered, + vals.refresh_token_validity_seconds, vals.access_token_validity_seconds, vals.id_token_validity_seconds, vals.allow_introspection); + +MERGE INTO client_scope + USING (SELECT id, scope FROM client_scope_TEMP, client_details WHERE client_details.client_id = client_scope_TEMP.owner_id) vals + ON (vals.id = client_scope.owner_id AND vals.scope = client_scope.scope) + WHEN NOT MATCHED THEN + INSERT (owner_id, scope) values (vals.id, vals.scope); + +MERGE INTO client_redirect_uri + USING (SELECT id, redirect_uri FROM client_redirect_uri_TEMP, client_details WHERE client_details.client_id = client_redirect_uri_TEMP.owner_id) vals + ON (vals.id = client_redirect_uri.owner_id AND vals.redirect_uri = client_redirect_uri.redirect_uri) + WHEN NOT MATCHED THEN + INSERT (owner_id, redirect_uri) values (vals.id, vals.redirect_uri); + +MERGE INTO client_grant_type + USING (SELECT id, grant_type FROM client_grant_type_TEMP, client_details WHERE client_details.client_id = client_grant_type_TEMP.owner_id) vals + ON (vals.id = client_grant_type.owner_id AND vals.grant_type = client_grant_type.grant_type) + WHEN NOT MATCHED THEN + INSERT (owner_id, grant_type) values (vals.id, vals.grant_type); diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/create_db-user b/openid-connect-server-webapp/src/main/resources/db/oracle/create_db-user new file mode 100644 index 0000000000..fdbf9d44fb --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/create_db-user @@ -0,0 +1,15 @@ +drop user oauth cascade; +drop tablespace data_ts INCLUDING CONTENTS AND DATAFILES; +drop tablespace temp_ts INCLUDING CONTENTS AND DATAFILES; +CREATE TABLESPACE data_ts DATAFILE 'data_ts.dat' SIZE 40M ONLINE; +CREATE TEMPORARY TABLESPACE temp_ts TEMPFILE 'temp_ts.dbf' SIZE 5M AUTOEXTEND ON; +create user oauth identified by test DEFAULT TABLESPACE data_ts QUOTA 500K ON data_ts TEMPORARY TABLESPACE temp_ts; +GRANT CONNECT TO oauth; +GRANT UNLIMITED TABLESPACE TO oauth; +grant create session to oauth; +grant create table to oauth; +GRANT CREATE TABLESPACE TO oauth; +GRANT CREATE VIEW TO oauth; +GRANT CREATE ANY INDEX TO oauth; +GRANT CREATE SEQUENCE TO oauth; +GRANT CREATE SYNONYM TO oauth; diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/entity-mappings_oracle.xml b/openid-connect-server-webapp/src/main/resources/db/oracle/entity-mappings_oracle.xml new file mode 100644 index 0000000000..2aba62824f --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/entity-mappings_oracle.xml @@ -0,0 +1,281 @@ + + + + OpenID Connect Server entities + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/loading_temp_tables_oracle.sql b/openid-connect-server-webapp/src/main/resources/db/oracle/loading_temp_tables_oracle.sql new file mode 100644 index 0000000000..c9a1e7f3d6 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/loading_temp_tables_oracle.sql @@ -0,0 +1,77 @@ +-- +-- Temporary tables used during the bootstrapping process to safely load users and clients. +-- These are not needed if you're not using the users.sql/clients.sql files to bootstrap the database. +-- + +CREATE GLOBAL TEMPORARY TABLE authorities_TEMP ( + username varchar2(50) not null, + authority varchar2(50) not null, + constraint ix_authority_TEMP unique (username,authority) +) ON COMMIT PRESERVE ROWS; + +CREATE GLOBAL TEMPORARY TABLE users_TEMP ( + username VARCHAR2(50) not null primary key, + password VARCHAR2(50) not null, + enabled NUMBER(1) not null +) ON COMMIT PRESERVE ROWS; + +CREATE GLOBAL TEMPORARY TABLE user_info_TEMP ( + sub VARCHAR2(256) not null primary key, + preferred_username VARCHAR2(256), + name VARCHAR2(256), + given_name VARCHAR2(256), + family_name VARCHAR2(256), + middle_name VARCHAR2(256), + nickname VARCHAR2(256), + profile VARCHAR2(256), + picture VARCHAR2(256), + website VARCHAR2(256), + email VARCHAR2(256), + email_verified NUMBER(1), + gender VARCHAR2(256), + zone_info VARCHAR2(256), + locale VARCHAR2(256), + phone_number VARCHAR2(256), + address_id VARCHAR2(256), + updated_time VARCHAR2(256), + birthdate VARCHAR2(256) +) ON COMMIT PRESERVE ROWS; + +CREATE GLOBAL TEMPORARY TABLE client_details_TEMP ( + client_description VARCHAR2(256), + dynamically_registered NUMBER(1), + id_token_validity_seconds NUMBER(19), + + client_id VARCHAR2(256), + client_secret VARCHAR2(2048), + access_token_validity_seconds NUMBER(19), + refresh_token_validity_seconds NUMBER(19), + allow_introspection NUMBER(1), + + client_name VARCHAR2(256) +) ON COMMIT PRESERVE ROWS; + +CREATE GLOBAL TEMPORARY TABLE client_scope_TEMP ( + owner_id VARCHAR2(256), + scope VARCHAR2(2048) +) ON COMMIT PRESERVE ROWS; + +CREATE GLOBAL TEMPORARY TABLE client_redirect_uri_TEMP ( + owner_id VARCHAR2(256), + redirect_uri VARCHAR2(2048) +) ON COMMIT PRESERVE ROWS; + +CREATE GLOBAL TEMPORARY TABLE client_grant_type_TEMP ( + owner_id VARCHAR2(256), + grant_type VARCHAR2(2000) +) ON COMMIT PRESERVE ROWS; + +CREATE GLOBAL TEMPORARY TABLE system_scope_TEMP ( + scope VARCHAR2(256), + description VARCHAR2(4000), + icon VARCHAR2(256), + restricted NUMBER(1), + default_scope NUMBER(1), + structured NUMBER(1), + structured_param_description VARCHAR2(256) +) ON COMMIT PRESERVE ROWS; diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/oracle_database_index.sql b/openid-connect-server-webapp/src/main/resources/db/oracle/oracle_database_index.sql new file mode 100644 index 0000000000..fc70a7ae41 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/oracle_database_index.sql @@ -0,0 +1,18 @@ +-- +-- Indexes for Oracle +-- + +CREATE INDEX at_tv_idx ON access_token(token_value); +CREATE INDEX ts_oi_idx ON token_scope(owner_id); +CREATE INDEX at_exp_idx ON access_token(expiration); +CREATE INDEX rf_ahi_idx ON refresh_token(auth_holder_id); +CREATE INDEX rf_tv_idx ON refresh_token(token_value); +CREATE INDEX at_ahi_idx ON access_token(auth_holder_id); +CREATE INDEX aha_oi_idx ON authentication_holder_authority(owner_id); +CREATE INDEX ahe_oi_idx ON authentication_holder_extension(owner_id); +CREATE INDEX ahrp_oi_idx ON authentication_holder_request_parameter(owner_id); +CREATE INDEX ahri_oi_idx ON authentication_holder_resource_id(owner_id); +CREATE INDEX ahrt_oi_idx ON authentication_holder_response_type(owner_id); +CREATE INDEX ahs_oi_idx ON authentication_holder_scope(owner_id); +CREATE INDEX ac_ahi_idx ON authorization_code(auth_holder_id); +CREATE INDEX suaa_oi_idx ON saved_user_auth_authority(owner_id); diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/oracle_database_tables.sql b/openid-connect-server-webapp/src/main/resources/db/oracle/oracle_database_tables.sql new file mode 100644 index 0000000000..9f430adace --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/oracle_database_tables.sql @@ -0,0 +1,417 @@ +-- +-- Tables for OIDC Server functionality, Oracle +-- + +CREATE TABLE access_token ( + id NUMBER(19) NOT NULL PRIMARY KEY, + token_value VARCHAR2(4000), + expiration TIMESTAMP, + token_type VARCHAR2(256), + refresh_token_id NUMBER(19), + client_id NUMBER(19), + auth_holder_id NUMBER(19), + approved_site_id NUMBER(19) +); +CREATE SEQUENCE access_token_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE access_token_permissions ( + access_token_id NUMBER(19) NOT NULL, + permission_id NUMBER(19) NOT NULL +); + +CREATE TABLE address ( + id NUMBER(19) NOT NULL PRIMARY KEY, + formatted VARCHAR2(256), + street_address VARCHAR2(256), + locality VARCHAR2(256), + region VARCHAR2(256), + postal_code VARCHAR2(256), + country VARCHAR2(256) +); +CREATE SEQUENCE address_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE approved_site ( + id NUMBER(19) NOT NULL PRIMARY KEY, + user_id VARCHAR2(256), + client_id VARCHAR2(256), + creation_date TIMESTAMP, + access_date TIMESTAMP, + timeout_date TIMESTAMP, + whitelisted_site_id NUMBER(19) +); +CREATE SEQUENCE approved_site_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE approved_site_scope ( + owner_id NUMBER(19), + scope VARCHAR2(256) +); + +CREATE TABLE authentication_holder ( + id NUMBER(19) NOT NULL PRIMARY KEY, + user_auth_id NUMBER(19), + approved NUMBER(1), + redirect_uri VARCHAR2(2048), + client_id VARCHAR2(256), + + CONSTRAINT approved_check CHECK (approved in (1,0)) +); +CREATE SEQUENCE authentication_holder_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE auth_holder_authority ( + owner_id NUMBER(19), + authority VARCHAR2(256) +); + +CREATE TABLE auth_holder_resource_id ( + owner_id NUMBER(19), + resource_id VARCHAR2(2048) +); + +CREATE TABLE auth_holder_response_type ( + owner_id NUMBER(19), + response_type VARCHAR2(2048) +); + +CREATE TABLE auth_holder_extension ( + owner_id NUMBER(19), + extension VARCHAR2(2048), + val VARCHAR2(2048) +); + +CREATE TABLE authentication_holder_scope ( + owner_id NUMBER(19), + scope VARCHAR2(2048) +); + +CREATE TABLE auth_holder_request_parameter ( + owner_id NUMBER(19), + param VARCHAR2(2048), + val VARCHAR2(2048) +); + +CREATE TABLE saved_user_auth ( + id NUMBER(19) NOT NULL PRIMARY KEY, + name VARCHAR2(1024), + authenticated NUMBER(1), + source_class VARCHAR2(2048), + + CONSTRAINT authenticated_check CHECK (authenticated in (1,0)) +); +CREATE SEQUENCE saved_user_auth_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE saved_user_auth_authority ( + owner_id NUMBER(19), + authority VARCHAR2(256) +); + +CREATE TABLE client_authority ( + owner_id NUMBER(19), + authority VARCHAR2(256) +); + +CREATE TABLE authorization_code ( + id NUMBER(19) NOT NULL PRIMARY KEY, + code VARCHAR2(256), + auth_holder_id NUMBER(19), + expiration TIMESTAMP +); +CREATE SEQUENCE authorization_code_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE client_grant_type ( + owner_id NUMBER(19), + grant_type VARCHAR2(2000) +); + +CREATE TABLE client_response_type ( + owner_id NUMBER(19), + response_type VARCHAR2(2000) +); + +CREATE TABLE blacklisted_site ( + id NUMBER(19) NOT NULL PRIMARY KEY, + uri VARCHAR2(2048) +); +CREATE SEQUENCE blacklisted_site_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE client_details ( + id NUMBER(19) NOT NULL PRIMARY KEY, + + client_description VARCHAR2(1024), + reuse_refresh_tokens NUMBER(1) DEFAULT 1 NOT NULL, + dynamically_registered NUMBER(1) DEFAULT 0 NOT NULL, + allow_introspection NUMBER(1) DEFAULT 0 NOT NULL, + id_token_validity_seconds NUMBER(19) DEFAULT 600 NOT NULL, + + client_id VARCHAR2(256), + client_secret VARCHAR2(2048), + access_token_validity_seconds NUMBER(19), + refresh_token_validity_seconds NUMBER(19), + device_code_validity_seconds NUMBER(19), + + application_type VARCHAR2(256), + client_name VARCHAR2(256), + token_endpoint_auth_method VARCHAR2(256), + subject_type VARCHAR2(256), + + logo_uri VARCHAR2(2048), + policy_uri VARCHAR2(2048), + client_uri VARCHAR2(2048), + tos_uri VARCHAR2(2048), + + jwks_uri VARCHAR2(2048), + jwks CLOB, + sector_identifier_uri VARCHAR2(2048), + + request_object_signing_alg VARCHAR2(256), + + user_info_signed_response_alg VARCHAR2(256), + user_info_encrypted_resp_alg VARCHAR2(256), + user_info_encrypted_resp_enc VARCHAR2(256), + + id_token_signed_response_alg VARCHAR2(256), + id_token_encrypted_resp_alg VARCHAR2(256), + id_token_encrypted_resp_enc VARCHAR2(256), + + token_endpoint_auth_sign_alg VARCHAR2(256), + + default_max_age NUMBER(19), + require_auth_time NUMBER(1), + created_at TIMESTAMP, + initiate_login_uri VARCHAR2(2048), + clear_access_tokens_on_refresh NUMBER(1) DEFAULT 1 NOT NULL, + + software_statement VARCHAR(4096), + software_id VARCHAR(2048), + software_statement VARCHAR2(4000), + + code_challenge_method VARCHAR2(256), + + CONSTRAINT client_details_unique UNIQUE (client_id), + CONSTRAINT reuse_refresh_tokens_check CHECK (reuse_refresh_tokens in (1,0)), + CONSTRAINT dynamically_registered_check CHECK (dynamically_registered in (1,0)), + CONSTRAINT allow_introspection_check CHECK (allow_introspection in (1,0)), + CONSTRAINT require_auth_time_check CHECK (require_auth_time in (1,0)), + CONSTRAINT clear_acc_tok_on_refresh_check CHECK (clear_access_tokens_on_refresh in (1,0)) +); +CREATE SEQUENCE client_details_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE client_request_uri ( + owner_id NUMBER(19), + request_uri VARCHAR2(2000) +); + +CREATE TABLE client_post_logout_redir_uri ( + owner_id NUMBER(19), + post_logout_redirect_uri VARCHAR2(2000) +); + +CREATE TABLE client_default_acr_value ( + owner_id NUMBER(19), + default_acr_value VARCHAR2(2000) +); + +CREATE TABLE client_contact ( + owner_id NUMBER(19), + contact VARCHAR2(256) +); + +CREATE TABLE client_redirect_uri ( + owner_id NUMBER(19), + redirect_uri VARCHAR2(2048) +); + +CREATE TABLE client_claims_redirect_uri ( + owner_id NUMBER(19), + redirect_uri VARCHAR2(2048) +); + +CREATE TABLE refresh_token ( + id NUMBER(19) NOT NULL PRIMARY KEY, + token_value VARCHAR2(4000), + expiration TIMESTAMP, + auth_holder_id NUMBER(19), + client_id NUMBER(19) +); +CREATE SEQUENCE refresh_token_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE client_resource ( + owner_id NUMBER(19), + resource_id VARCHAR2(256) +); + +CREATE TABLE client_scope ( + owner_id NUMBER(19), + scope VARCHAR2(2048) +); + +CREATE TABLE token_scope ( + owner_id NUMBER(19), + scope VARCHAR2(2048) +); + +CREATE TABLE system_scope ( + id NUMBER(19) NOT NULL PRIMARY KEY, + scope VARCHAR2(256) NOT NULL, + description VARCHAR2(4000), + icon VARCHAR2(256), + restricted NUMBER(1) DEFAULT 0 NOT NULL, + default_scope NUMBER(1) DEFAULT 0 NOT NULL + + CONSTRAINT system_scope_unique UNIQUE (scope), + CONSTRAINT default_scope_check CHECK (default_scope in (1,0)), + CONSTRAINT restricted_check CHECK (restricted in (1,0)) +); +CREATE SEQUENCE system_scope_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE user_info ( + id NUMBER(19) NOT NULL PRIMARY KEY, + sub VARCHAR2(256), + preferred_username VARCHAR2(256), + name VARCHAR2(256), + given_name VARCHAR2(256), + family_name VARCHAR2(256), + middle_name VARCHAR2(256), + nickname VARCHAR2(256), + profile VARCHAR2(256), + picture VARCHAR2(256), + website VARCHAR2(256), + email VARCHAR2(256), + email_verified NUMBER(1), + gender VARCHAR2(256), + zone_info VARCHAR2(256), + locale VARCHAR2(256), + phone_number VARCHAR2(256), + phone_number_verified NUMBER(1), + address_id VARCHAR2(256), + updated_time VARCHAR2(256), + birthdate VARCHAR2(256), + src VARCHAR2(4000), + + CONSTRAINT email_verified_check CHECK (email_verified in (1,0)), + CONSTRAINT phone_number_verified_check CHECK (phone_number_verified in (1,0)) +); +CREATE SEQUENCE user_info_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE whitelisted_site ( + id NUMBER(19) NOT NULL PRIMARY KEY, + creator_user_id VARCHAR2(256), + client_id VARCHAR2(256) +); +CREATE SEQUENCE whitelisted_site_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE whitelisted_site_scope ( + owner_id NUMBER(19), + scope VARCHAR2(256) +); + +CREATE TABLE pairwise_identifier ( + id NUMBER(19) NOT NULL PRIMARY KEY, + identifier VARCHAR2(256), + sub VARCHAR2(256), + sector_identifier VARCHAR2(2048) +); +CREATE SEQUENCE pairwise_identifier_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE resource_set ( + id NUMBER(19) NOT NULL PRIMARY KEY, + name VARCHAR2(1024) NOT NULL, + uri VARCHAR2(1024), + icon_uri VARCHAR2(1024), + rs_type VARCHAR2(256), + owner VARCHAR2(256) NOT NULL, + client_id VARCHAR2(256) +); +CREATE SEQUENCE resource_set_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE resource_set_scope ( + owner_id NUMBER(19) NOT NULL, + scope VARCHAR2(256) NOT NULL +); + +CREATE TABLE permission_ticket ( + id NUMBER(19) NOT NULL PRIMARY KEY, + ticket VARCHAR2(256) NOT NULL, + permission_id NUMBER(19) NOT NULL, + expiration TIMESTAMP +); +CREATE SEQUENCE permission_ticket_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE permission ( + id NUMBER(19) NOT NULL PRIMARY KEY, + resource_set_id NUMBER(19) +); +CREATE SEQUENCE permission_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE permission_scope ( + owner_id NUMBER(19) NOT NULL, + scope VARCHAR2(256) NOT NULL +); + +CREATE TABLE claim ( + id NUMBER(19) NOT NULL PRIMARY KEY, + name VARCHAR2(256), + friendly_name VARCHAR2(1024), + claim_type VARCHAR2(1024), + claim_value VARCHAR2(1024) +); +CREATE SEQUENCE claim_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE claim_to_policy ( + policy_id NUMBER(19) NOT NULL, + claim_id NUMBER(19) NOT NULL +); + +CREATE TABLE claim_to_permission_ticket ( + permission_ticket_id NUMBER(19) NOT NULL, + claim_id NUMBER(19) NOT NULL +); + +CREATE TABLE policy ( + id NUMBER(19) NOT NULL PRIMARY KEY, + name VARCHAR2(1024), + resource_set_id NUMBER(19) +); +CREATE SEQUENCE policy_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE policy_scope ( + owner_id NUMBER(19) NOT NULL, + scope VARCHAR2(256) NOT NULL +); + +CREATE TABLE claim_token_format ( + owner_id NUMBER(19) NOT NULL, + claim_token_format VARCHAR2(1024) NOT NULL +); + +CREATE TABLE claim_issuer ( + owner_id NUMBER(19) NOT NULL, + issuer VARCHAR2(1024) NOT NULL +); + +CREATE TABLE saved_registered_client ( + id NUMBER(19) NOT NULL PRIMARY KEY, + issuer VARCHAR2(1024), + registered_client CLOB +); +CREATE SEQUENCE saved_registered_client_seq START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE IF NOT EXISTS device_code ( + id NUMBER(19) NOT NULL PRIMARY KEY, + device_code VARCHAR2(1024), + user_code VARCHAR2(1024), + expiration TIMESTAMP, + client_id VARCHAR2(256), + approved BOOLEAN, + auth_holder_id NUMBER(19) +); + +CREATE TABLE IF NOT EXISTS device_code_scope ( + owner_id NUMBER(19) NOT NULL, + scope VARCHAR2(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS device_code_request_parameter ( + owner_id NUMBER(19), + param VARCHAR2(2048), + val VARCHAR2(2048) +); diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/scopes_oracle.sql b/openid-connect-server-webapp/src/main/resources/db/oracle/scopes_oracle.sql new file mode 100644 index 0000000000..bb6bc82a23 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/scopes_oracle.sql @@ -0,0 +1,26 @@ +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('openid', 'log in using your identity', 'user', 0, 1); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('profile', 'basic profile information', 'list-alt', 0, 1); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('email', 'email address', 'envelope', 0, 1); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('address', 'physical address', 'home', 0, 1); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('phone', 'telephone number', 'bell', 0, 1, 0); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('offline_access', 'offline access', 'time', 0, 0); +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +MERGE INTO system_scope + USING (SELECT scope, description, icon, restricted, default_scope FROM system_scope_TEMP) vals + ON (vals.scope = system_scope.scope) + WHEN NOT MATCHED THEN + INSERT (id, scope, description, icon, restricted, default_scope) VALUES(system_scope_seq.nextval, vals.scope, + vals.description, vals.icon, vals.restricted, vals.default_scope); diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/security-schema_oracle.sql b/openid-connect-server-webapp/src/main/resources/db/oracle/security-schema_oracle.sql new file mode 100644 index 0000000000..5b67ef668f --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/security-schema_oracle.sql @@ -0,0 +1,18 @@ +-- +-- Tables for Spring Security's user details service +-- + +create table users( + username varchar2(50) not null primary key, + password varchar2(50) not null, + enabled number(1) not null, + + constraint enabled_check check (enabled in (1, 0)) +); + +create table authorities ( + username varchar2(50) not null, + authority varchar2(50) not null, + constraint fk_authorities_users foreign key(username) references users(username), + constraint ix_authority unique (username,authority) +); diff --git a/openid-connect-server-webapp/src/main/resources/db/oracle/users_oracle.sql b/openid-connect-server-webapp/src/main/resources/db/oracle/users_oracle.sql new file mode 100644 index 0000000000..732a13f16e --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/oracle/users_oracle.sql @@ -0,0 +1,39 @@ +-- +-- Insert user information into the temporary tables. To add users to the Oracle database, edit things here. +-- + +INSERT INTO users_TEMP (username, password, enabled) VALUES ('admin','password',1); +INSERT INTO users_TEMP (username, password, enabled) VALUES ('user','password',1); + + +INSERT INTO authorities_TEMP (username, authority) VALUES ('admin','ROLE_ADMIN'); +INSERT INTO authorities_TEMP (username, authority) VALUES('admin','ROLE_USER'); +INSERT INTO authorities_TEMP (username, authority) VALUES('user','ROLE_USER'); + +-- By default, the username column here has to match the username column in the users table, above +INSERT INTO user_info_TEMP (sub, preferred_username, name, email, email_verified) VALUES ('90342.ASDFJWFA','admin','Demo Admin','admin@example.com', 1); +INSERT INTO user_info_TEMP (sub, preferred_username, name, email, email_verified) VALUES ('01921.FLANRJQW','user','Demo User','user@example.com', 1); + + +-- +-- Merge the temporary users safely into the database. This is a two-step process to keep users from being created on every startup with a persistent store. +-- + +MERGE INTO users + USING (SELECT username, password, enabled FROM users_TEMP) vals + ON (vals.username = users.username) + WHEN NOT MATCHED THEN + INSERT (username, password, enabled) VALUES(vals.username, vals.password, vals.enabled); + +MERGE INTO authorities + USING (SELECT username, authority FROM authorities_TEMP) vals + ON (vals.username = authorities.username AND vals.authority = authorities.authority) + WHEN NOT MATCHED THEN + INSERT (username,authority) values (vals.username, vals.authority); + +MERGE INTO user_info + USING (SELECT sub, preferred_username, name, email, email_verified FROM user_info_TEMP) vals + ON (vals.preferred_username = user_info.preferred_username) + WHEN NOT MATCHED THEN + INSERT (id, sub, preferred_username, name, email, email_verified) VALUES (user_info_seq.nextval, vals.sub, vals.preferred_username, vals.name, vals.email, + vals.email_verified); diff --git a/openid-connect-server-webapp/src/main/resources/db/psql/clients.sql b/openid-connect-server-webapp/src/main/resources/db/psql/clients.sql new file mode 100644 index 0000000000..bf14c2b2b6 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/psql/clients.sql @@ -0,0 +1,66 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +--SET AUTOCOMMIT = OFF; + +START TRANSACTION; + +-- +-- Insert client information into the temporary tables. To add clients to the HSQL database, edit things here. +-- + +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('client', 'secret', 'Test Client', false, null, 3600, 600, true); + +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES + ('client', 'openid'), + ('client', 'profile'), + ('client', 'email'), + ('client', 'address'), + ('client', 'phone'), + ('client', 'offline_access'); + +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES + ('client', 'http://localhost/'), + ('client', 'http://localhost:8080/'); + +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES + ('client', 'authorization_code'), + ('client', 'urn:ietf:params:oauth:grant_type:redelegate'), + ('client', 'implicit'), + ('client', 'refresh_token'); + +-- +-- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. +-- + +INSERT INTO client_details (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) + SELECT client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection FROM client_details_TEMP + ON CONFLICT + DO NOTHING; + +INSERT INTO client_scope (scope) + SELECT scope FROM client_scope_TEMP, client_details WHERE client_details.client_id = client_scope_TEMP.owner_id + ON CONFLICT + DO NOTHING; + +INSERT INTO client_redirect_uri (redirect_uri) + SELECT redirect_uri FROM client_redirect_uri_TEMP, client_details WHERE client_details.client_id = client_redirect_uri_TEMP.owner_id + ON CONFLICT + DO NOTHING; + +INSERT INTO client_grant_type (grant_type) + SELECT grant_type FROM client_grant_type_TEMP, client_details WHERE client_details.client_id = client_grant_type_TEMP.owner_id + ON CONFLICT + DO NOTHING; + +-- +-- Close the transaction and turn autocommit back on +-- + +COMMIT; + +--SET AUTOCOMMIT = ON; + + diff --git a/openid-connect-server-webapp/src/main/resources/db/psql/psql_database_index.sql b/openid-connect-server-webapp/src/main/resources/db/psql/psql_database_index.sql new file mode 100644 index 0000000000..a641ff8211 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/psql/psql_database_index.sql @@ -0,0 +1,19 @@ +-- +-- Indexes for PostgreSQL +-- + +CREATE INDEX IF NOT EXISTS at_tv_idx ON access_token(token_value); +CREATE INDEX IF NOT EXISTS ts_oi_idx ON token_scope(owner_id); +CREATE INDEX IF NOT EXISTS at_exp_idx ON access_token(expiration); +CREATE INDEX IF NOT EXISTS rf_ahi_idx ON refresh_token(auth_holder_id); +CREATE INDEX IF NOT EXISTS rf_tv_idx ON refresh_token(token_value); +CREATE INDEX IF NOT EXISTS cd_ci_idx ON client_details(client_id); +CREATE INDEX IF NOT EXISTS at_ahi_idx ON access_token(auth_holder_id); +CREATE INDEX IF NOT EXISTS aha_oi_idx ON authentication_holder_authority(owner_id); +CREATE INDEX IF NOT EXISTS ahe_oi_idx ON authentication_holder_extension(owner_id); +CREATE INDEX IF NOT EXISTS ahrp_oi_idx ON authentication_holder_request_parameter(owner_id); +CREATE INDEX IF NOT EXISTS ahri_oi_idx ON authentication_holder_resource_id(owner_id); +CREATE INDEX IF NOT EXISTS ahrt_oi_idx ON authentication_holder_response_type(owner_id); +CREATE INDEX IF NOT EXISTS ahs_oi_idx ON authentication_holder_scope(owner_id); +CREATE INDEX IF NOT EXISTS ac_ahi_idx ON authorization_code(auth_holder_id); +CREATE INDEX IF NOT EXISTS suaa_oi_idx ON saved_user_auth_authority(owner_id); diff --git a/openid-connect-server-webapp/src/main/resources/db/psql/psql_database_tables.sql b/openid-connect-server-webapp/src/main/resources/db/psql/psql_database_tables.sql new file mode 100644 index 0000000000..be871b7e80 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/psql/psql_database_tables.sql @@ -0,0 +1,384 @@ +-- +-- Tables for OIDC Server functionality, PostgreSQL +-- + +CREATE TABLE IF NOT EXISTS access_token ( + id BIGSERIAL PRIMARY KEY, + token_value VARCHAR(4096), + expiration TIMESTAMP, + token_type VARCHAR(256), + refresh_token_id BIGINT, + client_id BIGINT, + auth_holder_id BIGINT, + approved_site_id BIGINT, + UNIQUE(token_value) +); + +CREATE TABLE IF NOT EXISTS access_token_permissions ( + access_token_id BIGINT NOT NULL, + permission_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS address ( + id BIGSERIAL PRIMARY KEY, + formatted VARCHAR(256), + street_address VARCHAR(256), + locality VARCHAR(256), + region VARCHAR(256), + postal_code VARCHAR(256), + country VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS approved_site ( + id BIGSERIAL PRIMARY KEY, + user_id VARCHAR(256), + client_id VARCHAR(256), + creation_date TIMESTAMP, + access_date TIMESTAMP, + timeout_date TIMESTAMP, + whitelisted_site_id BIGINT +); + +CREATE TABLE IF NOT EXISTS approved_site_scope ( + owner_id BIGINT, + scope VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authentication_holder ( + id BIGSERIAL PRIMARY KEY, + user_auth_id BIGINT, + approved BOOLEAN, + redirect_uri VARCHAR(2048), + client_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_authority ( + owner_id BIGINT, + authority VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_resource_id ( + owner_id BIGINT, + resource_id VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_response_type ( + owner_id BIGINT, + response_type VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_extension ( + owner_id BIGINT, + extension VARCHAR(2048), + val VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_scope ( + owner_id BIGINT, + scope VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS authentication_holder_request_parameter ( + owner_id BIGINT, + param VARCHAR(2048), + val VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS saved_user_auth ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(1024), + authenticated BOOLEAN, + source_class VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS saved_user_auth_authority ( + owner_id BIGINT, + authority VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS client_authority ( + owner_id BIGINT, + authority VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS authorization_code ( + id BIGSERIAL PRIMARY KEY, + code VARCHAR(256), + auth_holder_id BIGINT, + expiration TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS client_grant_type ( + owner_id BIGINT, + grant_type VARCHAR(2000) +); + +CREATE TABLE IF NOT EXISTS client_response_type ( + owner_id BIGINT, + response_type VARCHAR(2000) +); + +CREATE TABLE IF NOT EXISTS blacklisted_site ( + id BIGSERIAL PRIMARY KEY, + uri VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS client_details ( + id BIGSERIAL PRIMARY KEY, + + client_description VARCHAR(1024), + reuse_refresh_tokens BOOLEAN DEFAULT true NOT NULL, + dynamically_registered BOOLEAN DEFAULT false NOT NULL, + allow_introspection BOOLEAN DEFAULT false NOT NULL, + id_token_validity_seconds BIGINT DEFAULT 600 NOT NULL, + device_code_validity_seconds BIGINT, + + client_id VARCHAR(256), + client_secret VARCHAR(2048), + access_token_validity_seconds BIGINT, + refresh_token_validity_seconds BIGINT, + + application_type VARCHAR(256), + client_name VARCHAR(256), + token_endpoint_auth_method VARCHAR(256), + subject_type VARCHAR(256), + + logo_uri VARCHAR(2048), + policy_uri VARCHAR(2048), + client_uri VARCHAR(2048), + tos_uri VARCHAR(2048), + + jwks_uri VARCHAR(2048), + jwks VARCHAR(8192), + sector_identifier_uri VARCHAR(2048), + + request_object_signing_alg VARCHAR(256), + + user_info_signed_response_alg VARCHAR(256), + user_info_encrypted_response_alg VARCHAR(256), + user_info_encrypted_response_enc VARCHAR(256), + + id_token_signed_response_alg VARCHAR(256), + id_token_encrypted_response_alg VARCHAR(256), + id_token_encrypted_response_enc VARCHAR(256), + + token_endpoint_auth_signing_alg VARCHAR(256), + + default_max_age BIGINT, + require_auth_time BOOLEAN, + created_at TIMESTAMP, + initiate_login_uri VARCHAR(2048), + clear_access_tokens_on_refresh BOOLEAN DEFAULT true NOT NULL, + + software_statement VARCHAR(4096), + software_id VARCHAR(2048), + software_version VARCHAR(2048), + + code_challenge_method VARCHAR(256), + + UNIQUE (client_id) +); + +CREATE TABLE IF NOT EXISTS client_request_uri ( + owner_id BIGINT, + request_uri VARCHAR(2000) +); + +CREATE TABLE IF NOT EXISTS client_post_logout_redirect_uri ( + owner_id BIGINT, + post_logout_redirect_uri VARCHAR(2000) +); + +CREATE TABLE IF NOT EXISTS client_default_acr_value ( + owner_id BIGINT, + default_acr_value VARCHAR(2000) +); + +CREATE TABLE IF NOT EXISTS client_contact ( + owner_id BIGINT, + contact VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS client_redirect_uri ( + owner_id BIGINT, + redirect_uri VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS client_claims_redirect_uri ( + owner_id BIGINT, + redirect_uri VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS refresh_token ( + id BIGSERIAL PRIMARY KEY, + token_value VARCHAR(4096), + expiration TIMESTAMP, + auth_holder_id BIGINT, + client_id BIGINT +); + +CREATE TABLE IF NOT EXISTS client_resource ( + owner_id BIGINT, + resource_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS client_scope ( + owner_id BIGINT, + scope VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS token_scope ( + owner_id BIGINT, + scope VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS system_scope ( + id BIGSERIAL PRIMARY KEY, + scope VARCHAR(256) NOT NULL, + description VARCHAR(4096), + icon VARCHAR(256), + restricted BOOLEAN DEFAULT false NOT NULL, + default_scope BOOLEAN DEFAULT false NOT NULL, + UNIQUE (scope) +); + +CREATE TABLE IF NOT EXISTS user_info ( + id BIGSERIAL PRIMARY KEY, + sub VARCHAR(256), + preferred_username VARCHAR(256), + name VARCHAR(256), + given_name VARCHAR(256), + family_name VARCHAR(256), + middle_name VARCHAR(256), + nickname VARCHAR(256), + profile VARCHAR(256), + picture VARCHAR(256), + website VARCHAR(256), + email VARCHAR(256), + email_verified BOOLEAN, + gender VARCHAR(256), + zone_info VARCHAR(256), + locale VARCHAR(256), + phone_number VARCHAR(256), + phone_number_verified BOOLEAN, + address_id VARCHAR(256), + updated_time VARCHAR(256), + birthdate VARCHAR(256), + src VARCHAR(4096) +); + +CREATE TABLE IF NOT EXISTS whitelisted_site ( + id BIGSERIAL PRIMARY KEY, + creator_user_id VARCHAR(256), + client_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS whitelisted_site_scope ( + owner_id BIGINT, + scope VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS pairwise_identifier ( + id BIGSERIAL PRIMARY KEY, + identifier VARCHAR(256), + sub VARCHAR(256), + sector_identifier VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS resource_set ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(1024) NOT NULL, + uri VARCHAR(1024), + icon_uri VARCHAR(1024), + rs_type VARCHAR(256), + owner VARCHAR(256) NOT NULL, + client_id VARCHAR(256) +); + +CREATE TABLE IF NOT EXISTS resource_set_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS permission_ticket ( + id BIGSERIAL PRIMARY KEY, + ticket VARCHAR(256) NOT NULL, + permission_id BIGINT NOT NULL, + expiration TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS permission ( + id BIGSERIAL PRIMARY KEY, + resource_set_id BIGINT +); + +CREATE TABLE IF NOT EXISTS permission_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(256), + friendly_name VARCHAR(1024), + claim_type VARCHAR(1024), + claim_value VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS claim_to_policy ( + policy_id BIGINT NOT NULL, + claim_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim_to_permission_ticket ( + permission_ticket_id BIGINT NOT NULL, + claim_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS policy ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(1024), + resource_set_id BIGINT +); + +CREATE TABLE IF NOT EXISTS policy_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS claim_token_format ( + owner_id BIGINT NOT NULL, + claim_token_format VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS claim_issuer ( + owner_id BIGINT NOT NULL, + issuer VARCHAR(1024) +); + +CREATE TABLE IF NOT EXISTS saved_registered_client ( + id BIGSERIAL PRIMARY KEY, + issuer VARCHAR(1024), + registered_client VARCHAR(8192) +); + +CREATE TABLE IF NOT EXISTS device_code ( + id BIGSERIAL PRIMARY KEY, + device_code VARCHAR(1024), + user_code VARCHAR(1024), + expiration TIMESTAMP NULL, + client_id VARCHAR(256), + approved BOOLEAN, + auth_holder_id BIGINT +); + +CREATE TABLE IF NOT EXISTS device_code_scope ( + owner_id BIGINT NOT NULL, + scope VARCHAR(256) NOT NULL +); + +CREATE TABLE IF NOT EXISTS device_code_request_parameter ( + owner_id BIGINT, + param VARCHAR(2048), + val VARCHAR(2048) +); diff --git a/openid-connect-server-webapp/src/main/resources/db/psql/scopes.sql b/openid-connect-server-webapp/src/main/resources/db/psql/scopes.sql new file mode 100644 index 0000000000..140c727554 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/psql/scopes.sql @@ -0,0 +1,33 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +--SET AUTOCOMMIT = OFF; + +START TRANSACTION; + +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('openid', 'log in using your identity', 'user', false, true), + ('profile', 'basic profile information', 'list-alt', false, true), + ('email', 'email address', 'envelope', false, true), + ('address', 'physical address', 'home', false, true), + ('phone', 'telephone number', 'bell', false, true), + ('offline_access', 'offline access', 'time', false, false); + +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +INSERT INTO system_scope (scope, description, icon, restricted, default_scope) + SELECT scope, description, icon, restricted, default_scope FROM system_scope_TEMP + ON CONFLICT(scope) + DO NOTHING; + +COMMIT; + +--SET AUTOCOMMIT = ON; + diff --git a/openid-connect-server-webapp/src/main/resources/db/psql/security-schema.sql b/openid-connect-server-webapp/src/main/resources/db/psql/security-schema.sql new file mode 100644 index 0000000000..bc5d70b880 --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/psql/security-schema.sql @@ -0,0 +1,14 @@ +-- +-- Tables for Spring Security's user details service +-- + +create table IF NOT EXISTS users( + username varchar(50) not null primary key, + password varchar(50) not null, + enabled boolean not null); + + create table IF NOT EXISTS authorities ( + username varchar(50) not null, + authority varchar(50) not null, + constraint fk_authorities_users foreign key(username) references users(username), + constraint ix_authority unique (username,authority)); \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/resources/db/psql/users.sql b/openid-connect-server-webapp/src/main/resources/db/psql/users.sql new file mode 100644 index 0000000000..537330278c --- /dev/null +++ b/openid-connect-server-webapp/src/main/resources/db/psql/users.sql @@ -0,0 +1,55 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +--SET AUTOCOMMIT FALSE; + +START TRANSACTION; + +-- +-- Insert user information into the temporary tables. To add users to the HSQL database, edit things here. +-- + +INSERT INTO users_TEMP (username, password, enabled) VALUES + ('admin','password',true), + ('user','password',true); + + +INSERT INTO authorities_TEMP (username, authority) VALUES + ('admin','ROLE_ADMIN'), + ('admin','ROLE_USER'), + ('user','ROLE_USER'); + +-- By default, the username column here has to match the username column in the users table, above +INSERT INTO user_info_TEMP (sub, preferred_username, name, email, email_verified) VALUES + ('90342.ASDFJWFA','admin','Demo Admin','admin@example.com', true), + ('01921.FLANRJQW','user','Demo User','user@example.com', true); + + +-- +-- Merge the temporary users safely into the database. This is a two-step process to keep users from being created on every startup with a persistent store. +-- + +INSERT INTO users + SELECT username, password, enabled FROM users_TEMP + ON CONFLICT(username) + DO NOTHING; + +INSERT INTO authorities + SELECT username, authority FROM authorities_TEMP + ON CONFLICT(username, authority) + DO NOTHING; + +INSERT INTO user_info (sub, preferred_username, name, email, email_verified) + SELECT sub, preferred_username, name, email, email_verified FROM user_info_TEMP + ON CONFLICT + DO NOTHING; + +-- +-- Close the transaction and turn autocommit back on +-- + +COMMIT; + +--SET AUTOCOMMIT TRUE; + diff --git a/openid-connect-server-webapp/src/main/resources/db/scopes.sql b/openid-connect-server-webapp/src/main/resources/db/scopes.sql deleted file mode 100644 index a7b6f5b1c6..0000000000 --- a/openid-connect-server-webapp/src/main/resources/db/scopes.sql +++ /dev/null @@ -1,33 +0,0 @@ --- --- Turn off autocommit and start a transaction so that we can use the temp tables --- - -SET AUTOCOMMIT FALSE; - -START TRANSACTION; - --- --- Insert scope information into the temporary tables. --- - -INSERT INTO system_scope_TEMP (scope, description, icon, allow_dyn_reg, default_scope, structured, structured_param_description) VALUES - ('openid', 'log in using your identity', 'user', true, true, false, null), - ('profile', 'basic profile information', 'list-alt', true, true, false, null), - ('email', 'email address', 'envelope', true, true, false, null), - ('address', 'physical address', 'home', true, true, false, null), - ('phone', 'telephone number', 'bell', true, true, false, null), - ('offline_access', 'offline access', 'time', true, true, false, null); - --- --- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. --- - -MERGE INTO system_scope - USING (SELECT scope, description, icon, allow_dyn_reg, default_scope, structured, structured_param_description FROM system_scope_TEMP) AS vals(scope, description, icon, allow_dyn_reg, default_scope, structured, structured_param_description) - ON vals.scope = system_scope.scope - WHEN NOT MATCHED THEN - INSERT (scope, description, icon, allow_dyn_reg, default_scope, structured, structured_param_description) VALUES(vals.scope, vals.description, vals.icon, vals.allow_dyn_reg, vals.default_scope, vals.structured, vals.structured_param_description); - -COMMIT; - -SET AUTOCOMMIT TRUE; \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/resources/db/upgrade/hsql_1.0-to-1.1_upgrade.sql b/openid-connect-server-webapp/src/main/resources/db/upgrade/hsql_1.0-to-1.1_upgrade.sql deleted file mode 100644 index fbdc94d60d..0000000000 --- a/openid-connect-server-webapp/src/main/resources/db/upgrade/hsql_1.0-to-1.1_upgrade.sql +++ /dev/null @@ -1,55 +0,0 @@ --- --- This SQL script will upgrade your MITREid Connect 1.0 database to a MITREid Connect 1.1 database in-place. --- --- NOTICE: Any authorization codes that have not been activated will be removed. --- - -ALTER TABLE access_token ADD COLUMN approved_site_id BIGINT; - -TRUNCATE TABLE authorization_code; - -ALTER TABLE authorization_code DROP COLUMN authorization_request_holder; - -ALTER TABLE authorization_code ADD COLUMN authentication LONGVARBINARY; - -ALTER TABLE client_details ALTER COLUMN id_token_validity_seconds SET NOT NULL; - -ALTER TABLE client_details ALTER COLUMN id_token_validity_seconds SET DEFAULT 600; - -ALTER TABLE client_details ADD COLUMN token_endpoint_auth_signing_alg VARCHAR(256); - -ALTER TABLE system_scope ADD COLUMN structured BOOLEAN NOT NULL DEFAULT false; - -ALTER TABLE system_scope ADD COLUMN structured_param_description VARCHAR(256); - -ALTER TABLE user_info ADD COLUMN phone_number_verified BOOLEAN; - -DROP TABLE client_nonce; - -DROP TABLE event; - -DROP TABLE IF EXISTS authorities_TEMP; - -DROP TABLE IF EXISTS users_TEMP; - -DROP TABLE IF EXISTS user_info_TEMP; - -DROP TABLE IF EXISTS client_details_TEMP; - -DROP TABLE IF EXISTS client_scope_TEMP; - -DROP TABLE IF EXISTS client_redirect_uri_TEMP; - -DROP TABLE IF EXISTS client_grant_type_TEMP; - -DROP TABLE IF EXISTS system_scope_TEMP; - - - - -CREATE TABLE IF NOT EXISTS pairwise_identifier ( - id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, - identifier VARCHAR(256), - sub VARCHAR(256), - sector_identifier VARCHAR(2048) -); diff --git a/openid-connect-server-webapp/src/main/resources/db/upgrade/mysql_1.0-to-1.1_upgrade.sql b/openid-connect-server-webapp/src/main/resources/db/upgrade/mysql_1.0-to-1.1_upgrade.sql deleted file mode 100644 index 40f24468f1..0000000000 --- a/openid-connect-server-webapp/src/main/resources/db/upgrade/mysql_1.0-to-1.1_upgrade.sql +++ /dev/null @@ -1,51 +0,0 @@ --- --- This SQL script will upgrade your MITREid Connect 1.0 database to a MITREid Connect 1.1 database in-place. --- --- NOTICE: Any authorization codes that have not been activated will be removed. --- - -ALTER TABLE access_token ADD COLUMN approved_site_id BIGINT; - -TRUNCATE TABLE authorization_code; - -ALTER TABLE authorization_code DROP COLUMN authorization_request_holder; - -ALTER TABLE authorization_code ADD COLUMN authentication LONGBLOB; - -ALTER TABLE client_details MODIFY id_token_validity_seconds BIGINT NOT NULL DEFAULT 600; - -ALTER TABLE client_details ADD COLUMN token_endpoint_auth_signing_alg VARCHAR(256); - -ALTER TABLE system_scope ADD COLUMN structured BOOLEAN NOT NULL DEFAULT 0; - -ALTER TABLE system_scope ADD COLUMN structured_param_description VARCHAR(256); - -ALTER TABLE user_info ADD COLUMN phone_number_verified BOOLEAN; - -DROP TABLE client_nonce; - -DROP TABLE event; - -DROP TEMPORARY TABLE IF EXISTS authorities_TEMP; - -DROP TEMPORARY TABLE IF EXISTS users_TEMP; - -DROP TEMPORARY TABLE IF EXISTS user_info_TEMP; - -DROP TEMPORARY TABLE IF EXISTS client_details_TEMP; - -DROP TEMPORARY TABLE IF EXISTS client_scope_TEMP; - -DROP TEMPORARY TABLE IF EXISTS client_redirect_uri_TEMP; - -DROP TEMPORARY TABLE IF EXISTS client_grant_type_TEMP; - -DROP TEMPORARY TABLE IF EXISTS system_scope_TEMP; - - -CREATE TABLE IF NOT EXISTS pairwise_identifier ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - identifier VARCHAR(256), - sub VARCHAR(256), - sector_identifier VARCHAR(2048) -); diff --git a/openid-connect-server-webapp/src/main/resources/keystore.jwks b/openid-connect-server-webapp/src/main/resources/keystore.jwks index d627ecfa32..461413ffe3 100644 --- a/openid-connect-server-webapp/src/main/resources/keystore.jwks +++ b/openid-connect-server-webapp/src/main/resources/keystore.jwks @@ -1,13 +1,12 @@ { - "keys": - [ - { - "alg": "RS256", - "d": "RkRPP2wJQQWOTTjgRnt4WLEl2rpo8Vpg586NhrE8QbJfu0XfrrTL6_pz0kCyqcFcy0Aq0QO5-Rn56M7aZFE5cqo8xq4pPM32IfvlKTuxQxW_0I9fod-dy9GMnhVCO9WKhXQ23H6vKYdq0gsI9ov_S1jIN7JPwdyzzLpAGXf12I0", - "e": "AQAB", - "n": "23zs5r8PQKpsKeoUd2Bjz3TJkUljWqMD8X98SaIb1LE7dCQzi9jwO58FGL0ieY1Dfnr9-g1iiY8sNzV-byawK98W9yFiopaghfoKtxXgUD8pi0fLPeWmAkntjn28Z_WZvvA265ELbBhphPXEJcFhdzUfgESHVuqFMEqp1pB-CP0", - "kty": "RSA", - "kid": "rsa1" - } - ] + "keys": [ + { + "alg": "RS256", + "d": "PvBAngE3kkTnD3yDKo3wCvHJHm20kb9a0FVGLd0s2Y0E_3H2XnZC8-2zPhN6AQTjPhohSDCew20gzm76lyOvMqRiUP2Zpaopa1d2fGvNIQSdM07yKa6EivEYxqPQxa5esoZnexgnb9fom70I8n5OQRNQikwu-az26CsHX2zWMRodzSdN5CXHvb1PV09DmH8azTYwoMElPIqmcTfxiRw2Ov5ucmXXngKRFJgvfUgKd7v4ScBX7sQoQEjWEtt7ta0WvL3Ar5E1RAW4aHxuubZ6AtloxWCf17AAKw03dfP5RDm5TDmgm2B635ecJ7fTvneFmg8W_fdMTPRfBlCGNBp3wQ", + "e": "AQAB", + "n": "qt6yOiI_wCoCVlGO0MySsez0VkSqhPvDl3rfabOslx35mYEO-n4ABfIT5Gn2zN-CeIcOZ5ugAXvIIRWv5H55-tzjFazi5IKkOIMCiz5__MtsdxKCqGlZu2zt-BLpqTOAPiflNPpM3RUAlxKAhnYEqNha6-allPnFQupnW_eTYoyuzuedT7dSp90ry0ZcQDimntXWeaSbrYKCj9Rr9W1jn2uTowUuXaScKXTCjAmJVnsD75JNzQfa8DweklTyWQF-Y5Ky039I0VIu-0CIGhXY48GAFe2EFb8VpNhf07DP63p138RWQ1d3KPEM9mYJVpQC68j3wzDQYSljpLf9by7TGw", + "kty": "RSA", + "kid": "rsa1" + } + ] } \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/resources/log4j.xml b/openid-connect-server-webapp/src/main/resources/log4j.xml index 27100bf2ee..caed28b323 100644 --- a/openid-connect-server-webapp/src/main/resources/log4j.xml +++ b/openid-connect-server-webapp/src/main/resources/log4j.xml @@ -1,20 +1,21 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> @@ -45,6 +46,12 @@ + + + + + + @@ -63,6 +70,10 @@ + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml index e6586f03f6..fdbc37ba72 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml @@ -1,20 +1,21 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -61,7 +102,7 @@ - + - - + + + - - - + + + + - - + + + + - + + - + + - + + - + + - + + + + + + + + + + - + + - + + - - + + - + + @@ -149,42 +216,61 @@ + + + + - - - - - - - - + + + + + + /introspect + /revoke + /token + + - + - + - + + - + + - + + + + + + - - + + + + + + + + @@ -224,6 +310,9 @@ + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/assertion-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/assertion-config.xml new file mode 100644 index 0000000000..59ea49fe90 --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/assertion-config.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/authz-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/authz-config.xml index ea2f5f5a47..0c5e5019f8 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/authz-config.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/authz-config.xml @@ -1,20 +1,19 @@ + Copyright 2018 The MIT Internet Trust Consortium + + 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. + --> + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> + token-endpoint-url="/token" + error-page="/error"> @@ -46,13 +46,15 @@ + - - + + + \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/crypto-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/crypto-config.xml index 9ba022718c..36c043a782 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/crypto-config.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/crypto-config.xml @@ -1,35 +1,46 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + xmlns:tx="http://www.springframework.org/schema/tx" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:security="http://www.springframework.org/schema/security" + xmlns:oauth="http://www.springframework.org/schema/security/oauth2" + xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + - + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/data-context.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/data-context.xml index a46c3c90c1..0313b5b1b5 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/data-context.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/data-context.xml @@ -1,66 +1,128 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> - + - - + + - + - + + If you are using a file based HSQLDB you should not run this every time. --> - + - + - - - - + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/endpoint-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/endpoint-config.xml new file mode 100644 index 0000000000..bcfc14a6c3 --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/endpoint-config.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/jpa-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/jpa-config.xml index aca6bf8d14..afe40844af 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/jpa-config.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/jpa-config.xml @@ -1,10 +1,36 @@ + + xmlns:tx="http://www.springframework.org/schema/tx" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:security="http://www.springframework.org/schema/security" + xmlns:oauth="http://www.springframework.org/schema/security/oauth2" + xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> + @@ -20,6 +46,9 @@ + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/local-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/local-config.xml index e8558403b1..e580f6e52a 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/local-config.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/local-config.xml @@ -1,23 +1,34 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + xmlns:tx="http://www.springframework.org/schema/tx" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:security="http://www.springframework.org/schema/security" + xmlns:oauth="http://www.springframework.org/schema/security/oauth2" + xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/locale-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/locale-config.xml new file mode 100644 index 0000000000..60cdb6b0f1 --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/locale-config.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/server-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/server-config.xml index 767e7c73c9..bf9f998652 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/server-config.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/server-config.xml @@ -1,36 +1,38 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + xmlns:context="http://www.springframework.org/schema/context" + xmlns:security="http://www.springframework.org/schema/security" + xmlns:oauth="http://www.springframework.org/schema/security/oauth2" + xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + @@ -41,6 +43,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/spring-servlet.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/spring-servlet.xml index 172b63cec1..f37e980ba6 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/spring-servlet.xml @@ -1,23 +1,34 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + xmlns:tx="http://www.springframework.org/schema/tx" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:security="http://www.springframework.org/schema/security" + xmlns:oauth="http://www.springframework.org/schema/security/oauth2" + xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/aboutContent.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/aboutContent.tag deleted file mode 100644 index 200ce5c77f..0000000000 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/aboutContent.tag +++ /dev/null @@ -1,5 +0,0 @@ -

About

-

This OpenID Connect service is built from the MITREid Connect Open Source project, from The MITRE Corporation and the MIT Kerberos and Internet Trust Consortium.

-

-More information about the project can be found at MITREid Connect on GitHub. There, you can submit bug reports, give feedback, or even contribute code patches for additional features you'd like to see. -

diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag index d421ffa1aa..d391a30ad0 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag @@ -1,19 +1,20 @@ <%@ tag language="java" pageEncoding="UTF-8"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> - -
  • Manage Clients
  • -
  • Whitelisted Clients
  • -
  • Blacklisted Clients
  • -
  • System Scopes
  • + +
  • +
  • +
  • +
  • - -
  • Manage Approved Sites
  • -
  • Manage Active Tokens
  • -
  • View Profile Information
  • + +
  • +
  • +
  • - -
  • Self-service client registration
  • -
  • Self-service protected resource registration
  • \ No newline at end of file +
  • +
  • +
  • \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/contactContent.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/contactContent.tag deleted file mode 100644 index efb538ff93..0000000000 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/contactContent.tag +++ /dev/null @@ -1,5 +0,0 @@ -

    Contact

    -

    -To report bugs with the MITREid Connect software itself, use the GitHub issue tracker. -For problems relating to this server, contact the server's administrator. -

    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/copyright.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/copyright.tag index f06786c659..4b0aa920ad 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/copyright.tag +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/copyright.tag @@ -1 +1,4 @@ -Powered by MITREid Connect ${project.version} © 2014 The MITRE Corporation and MIT KIT. +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +HEART Mode + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag index 0e7ec53aa3..2b95de6dcb 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag @@ -20,19 +20,26 @@ - + - - - - - - - + + + + - +
    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/header.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/header.tag index 7dc709ef8f..94f68be330 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/header.tag +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/header.tag @@ -1,9 +1,10 @@ <%@attribute name="title" required="false"%> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@ tag import="com.google.gson.Gson" %> - + @@ -13,6 +14,7 @@ + @@ -25,7 +27,7 @@ @@ -33,7 +35,20 @@ + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageAbout.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageAbout.tag deleted file mode 100644 index 7c9c72f159..0000000000 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageAbout.tag +++ /dev/null @@ -1,5 +0,0 @@ -

    About

    - -

    This OpenID Connect service is built from the MITREid Connect Open Source project, from The MITRE Corporation and the MIT Kerberos and Internet Trust Consortium.

    - -

    More »

    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageContact.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageContact.tag deleted file mode 100644 index 5d46247d2a..0000000000 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageContact.tag +++ /dev/null @@ -1,5 +0,0 @@ -

    Contact

    - -

    For more information or support, contact the administrators of this system.

    - -

    Email »

    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageStats.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageStats.tag deleted file mode 100644 index 9c48a3c6fd..0000000000 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageStats.tag +++ /dev/null @@ -1,44 +0,0 @@ -

    Current Statistics

    - -

    Loading statistics...

    - -

    There have been - ? user - of this system who have authorized - ? application, - with a total of - ? site approval.

    - - \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageWelcome.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageWelcome.tag deleted file mode 100644 index cdd991ffd2..0000000000 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/landingPageWelcome.tag +++ /dev/null @@ -1,11 +0,0 @@ -
    -
    - -
    -

    Welcome!

    - -

    OpenID Connect is an internet-scale federated identity protocol built on top of the OAuth2 authorization framework. OpenID Connect lets you log into a remote site using your identity without exposing your credentials, like a username and password.

    - -

    Learn more »

    -
    -
    \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/navmenu.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/navmenu.tag new file mode 100644 index 0000000000..78bfe15cb5 --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/navmenu.tag @@ -0,0 +1,39 @@ +<%@attribute name="pageName"%> +<%@ tag language="java" pageEncoding="UTF-8"%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> +<%@ taglib prefix="security" + uri="http://www.springframework.org/security/tags"%> + + + +
  • + + +
  • +
    + + + +
  • +
    + +
  • +
    +
    + + +
  • +
    + +
  • +
    +
    + + +
  • +
    + +
  • +
    +
    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/statsContent.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/statsContent.tag deleted file mode 100644 index e7c8cad4d8..0000000000 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/statsContent.tag +++ /dev/null @@ -1,8 +0,0 @@ -

    Current Statistics

    - -

    There have been - ${statsSummary["userCount"]} user${statsSummary["userCount"] == 1 ? "" : "s"} - of this system who have logged in to - ${statsSummary["clientCount"]} total site${statsSummary["clientCount"] == 1 ? "" : "s"}, - for a total of - ${statsSummary["approvalCount"]} site approval${statsSummary["approvalCount"] == 1 ? "" : "s"}.

    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/topbar.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/topbar.tag index f7a896d3e8..1bce4a1c5f 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/topbar.tag +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/topbar.tag @@ -1,6 +1,7 @@ <%@attribute name="pageName" required="false"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> @@ -17,7 +18,7 @@ - + @@ -33,73 +34,74 @@ - ${config.topbarTitle} + + + + ${config.shortTopbarTitle} + ${config.topbarTitle} + + + + \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/task-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/task-config.xml index adffe8786a..30f3e98a9a 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/task-config.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/task-config.xml @@ -1,24 +1,25 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> @@ -30,6 +31,8 @@ + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/ui-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/ui-config.xml new file mode 100644 index 0000000000..2e0cf646bb --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/ui-config.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + resources/js/client.js + resources/js/grant.js + resources/js/scope.js + resources/js/whitelist.js + resources/js/dynreg.js + resources/js/rsreg.js + resources/js/token.js + resources/js/blacklist.js + resources/js/profile.js + + + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/user-context.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/user-context.xml index 49d684ddb0..1bd665d398 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/user-context.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/user-context.xml @@ -1,54 +1,58 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + xmlns:context="http://www.springframework.org/schema/context" + xmlns:security="http://www.springframework.org/schema/security" + xmlns:oauth="http://www.springframework.org/schema/security/oauth2" + xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + + - - - + - - + - + + + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/about.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/about.jsp index 99751cb77e..1bf74f8b5e 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/about.jsp +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/about.jsp @@ -1,11 +1,12 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> -<%@ taglib prefix="security" - uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> - - + +
    @@ -14,7 +15,10 @@
    - +

    +

    + +

    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approve.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approve.jsp index a7a98d7bfa..6526fb8426 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approve.jsp +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approve.jsp @@ -1,26 +1,31 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ page import="org.springframework.security.core.AuthenticationException"%> <%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException"%> -<%@ page import="org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter"%> +<%@ page import="org.springframework.security.web.WebAttributes"%> <%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> - +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + +
    - <% if (session.getAttribute(AbstractAuthenticationProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY) != null && !(session.getAttribute(AbstractAuthenticationProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY) instanceof UnapprovedClientAuthenticationException)) { %> + <% if (session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) != null && !(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) instanceof UnapprovedClientAuthenticationException)) { %>
    × -

    Access could not be granted. - (<%= ((AuthenticationException) session.getAttribute(AbstractAuthenticationProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY)).getMessage() %>) +

    + (<%= ((AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).getMessage() %>)

    <% } %>
    -

    Approval Required for +

      @@ -32,27 +37,46 @@

    + action="${ config.issuer }${ config.issuer.endsWith('/') ? '' : '/' }authorize" method="post">
    - -

    This client was dynamically registered .

    +
    +

    + + + + +

    +
    ">

    - Caution: + :

    - This software was dynamically registered - and it has been approved - - time previously. + +

    + +

    +

    + + + + + + + + + + + +

    @@ -61,7 +85,7 @@
    • - +
    @@ -80,7 +104,7 @@
    - This client uses a pairwise identifier, which makes it more difficult to correlate your identity between sites. +
    - Access to: + :

    - Warning: + :

    - This client does not have any scopes registered and is therefore allowed to - request any scopes available on the system. Proceed with caution. +

    + +

    @@ -195,10 +218,6 @@ - - - - @@ -206,18 +225,18 @@
    - Remember this decision: + :
    @@ -226,7 +245,7 @@

    - Do you authorize + " @@ -236,12 +255,14 @@ "?

    - - - + + + +   -
    @@ -254,7 +275,11 @@ $(document).ready(function() { $('.claim-tooltip').popover(); - + $('.claim-tooltip').on('click', function(e) { + e.preventDefault(); + $(this).popover('show'); + }); + $(document).on('click', '#toggleMoreInformation', function(event) { event.preventDefault(); if ($('#moreInformation').is(':visible')) { @@ -270,24 +295,24 @@ $(document).ready(function() { } }); - var creationDate = ""; - var displayCreationDate = "Unknown"; + var creationDate = ""; + var displayCreationDate = $.t('approve.dynamically-registered-unkown'); var hoverCreationDate = ""; - if (creationDate == null || !moment(creationDate).isValid()) { - displayCreationDate = "Unknown"; - hoverCreationDate = ""; - } else { + if (creationDate != null && moment(creationDate).isValid()) { creationDate = moment(creationDate); if (moment().diff(creationDate, 'months') < 6) { displayCreationDate = creationDate.fromNow(); } else { - displayCreationDate = "on " + creationDate.format("MMMM Do, YYYY"); + displayCreationDate = "on " + creationDate.format("LL"); } - hoverCreationDate = creationDate.format("MMMM Do, YYYY [at] h:mmA") + hoverCreationDate = creationDate.format("LLL"); } $('#registrationTime').html(displayCreationDate); $('#registrationTime').attr('title', hoverCreationDate); + + + }); //--> diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approveDevice.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approveDevice.jsp new file mode 100644 index 0000000000..162170311e --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approveDevice.jsp @@ -0,0 +1,287 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ page import="org.springframework.security.core.AuthenticationException"%> +<%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException"%> +<%@ page import="org.springframework.security.web.WebAttributes"%> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + + + +
    + <% if (session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) != null && !(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) instanceof UnapprovedClientAuthenticationException)) { %> +
    + × + +

    + (<%= ((AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).getMessage() %>) +

    +
    + <% } %> + + +
    +

      + + + + + + + + +

    + + + +
    +
    + + + + +
    +

    + + + + +

    +
    +
    + + +
    "> +

    + : +

    + +

    + +

    +

    + + + + + + + + + + + +

    +
    +
    +
    +
    + + +
      +
    • + +
    • +
    + + +
    + +
    + + +
    + +
    +
    +
      + +
    • : ">
    • +
      + +
    • : ">
    • +
      + +
    • : ">
    • +
      + +
    • :
    • +
      +
    +
    +
    +
    +
    + + +
    + +
    +
    + +
    +
    +
    + : + + +
    +

    + : +

    +

    + +

    +
    +
    + +
      + +
    • + + + + + + + + + + + + + + +
    • + : + +
    • +
      +
    +
    + " + > + + + + + + + + + +
    + +
    + +
    +

    + + " + + + + + + + "? +

    + + + + + + +   + +
    + + + +
    +
    + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/contact.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/contact.jsp index 194ab4782f..cdfcdbcd16 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/contact.jsp +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/contact.jsp @@ -1,11 +1,13 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> -<%@ taglib prefix="security" - uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> - + +
    @@ -13,7 +15,10 @@
    - +

    +

    + +

    diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/deviceApproved.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/deviceApproved.jsp new file mode 100644 index 0000000000..80f601c633 --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/deviceApproved.jsp @@ -0,0 +1,39 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ page import="org.springframework.security.core.AuthenticationException"%> +<%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException"%> +<%@ page import="org.springframework.security.web.WebAttributes"%> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + + + +
    + +
    +

    + + + + + + + +

    + + + +
    +
    + +
    +
    +
    + +
    +
    + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/error.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/error.jsp new file mode 100644 index 0000000000..66c5f585ed --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/error.jsp @@ -0,0 +1,48 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@page import="org.springframework.http.HttpStatus"%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> +<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> +<%@page import="org.springframework.security.oauth2.common.exceptions.OAuth2Exception"%> +<% + +if (request.getAttribute("error") != null && request.getAttribute("error") instanceof OAuth2Exception) { + request.setAttribute("errorCode", ((OAuth2Exception)request.getAttribute("error")).getOAuth2ErrorCode()); + request.setAttribute("message", ((OAuth2Exception)request.getAttribute("error")).getMessage()); +} else if (request.getAttribute("javax.servlet.error.exception") != null) { + Throwable t = (Throwable)request.getAttribute("javax.servlet.error.exception"); + request.setAttribute("errorCode", t.getClass().getSimpleName() + " (" + request.getAttribute("javax.servlet.error.status_code") + ")"); + request.setAttribute("message", t.getMessage()); +} else if (request.getAttribute("javax.servlet.error.status_code") != null) { + Integer code = (Integer)request.getAttribute("javax.servlet.error.status_code"); + HttpStatus status = HttpStatus.valueOf(code); + request.setAttribute("errorCode", status.toString() + " " + status.getReasonPhrase()); + request.setAttribute("message", request.getAttribute("javax.servlet.error.message")); +} else { + request.setAttribute("errorCode", "Server error"); + request.setAttribute("message", "See the logs for details"); +} + +%> + + + +
    +
    +
    +
    +

    + +

    +

    + +

    +

    + +
    + +
    +
    +
    + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/home.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/home.jsp index f7615f2dc9..5fa2495a53 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/home.jsp +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/home.jsp @@ -1,23 +1,40 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> - + +
    - +
    +
    + +
    +

    +

    +
    +
    - +

    + +

    + +

    »

    - +

    +

    + +

    @@ -25,11 +42,43 @@
    - +

    + +

    + +

    + + + +

    + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/login.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/login.jsp index 5a15629e9a..5be8f9b2a5 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/login.jsp +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/login.jsp @@ -1,12 +1,15 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> @@ -57,17 +74,17 @@
    - +
    - +
    - +
    @@ -78,18 +95,66 @@ + + + + + + \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/resources/template/blacklist.html b/openid-connect-server-webapp/src/main/webapp/resources/template/blacklist.html new file mode 100644 index 0000000000..074fd0a0df --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/resources/template/blacklist.html @@ -0,0 +1,68 @@ + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/resources/template/client.html b/openid-connect-server-webapp/src/main/webapp/resources/template/client.html index dbc902e398..2a748fefbe 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/template/client.html +++ b/openid-connect-server-webapp/src/main/webapp/resources/template/client.html @@ -1,6 +1,7 @@ - + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html b/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html index 50f984a10c..eda228006d 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html +++ b/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html @@ -1,6 +1,7 @@ + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> - 4.0.0 - openid-connect-server - OpenID Connect Server Library - - org.mitre - openid-connect-parent - 1.1.10-SNAPSHOT - .. - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java-version} - ${java-version} - - - - - + 4.0.0 + openid-connect-server + OpenID Connect Server Library + + org.mitre + openid-connect-parent + 1.3.5-SNAPSHOT + .. + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java-version} + ${java-version} + + + + + org.mitre openid-connect-common - 1.1.10-SNAPSHOT - org.springframework - spring-tx - ${org.springframework-version} - - + org.springframework + spring-tx + + + org.springframework + spring-orm + test + + + commons-logging + commons-logging + + + + + org.eclipse.persistence + org.eclipse.persistence.core + + + org.hsqldb + hsqldb + test + + + org.eclipse.persistence + org.eclipse.persistence.jpa + test + + + + org.apache.commons + commons-io + - OpenID Connect server libraries for Spring and Spring Security. - + OpenID Connect server libraries for Spring and Spring Security. + diff --git a/openid-connect-server/src/main/java/org/mitre/discovery/view/WebfingerView.java b/openid-connect-server/src/main/java/org/mitre/discovery/view/WebfingerView.java index a3307f6f5e..493f769d3a 100644 --- a/openid-connect-server/src/main/java/org/mitre/discovery/view/WebfingerView.java +++ b/openid-connect-server/src/main/java/org/mitre/discovery/view/WebfingerView.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.discovery.view; @@ -26,6 +27,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.mitre.openid.connect.view.HttpCodeView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -47,30 +49,33 @@ @Component("webfingerView") public class WebfingerView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(WebfingerView.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(WebfingerView.class); private Gson gson = new GsonBuilder() - .setExclusionStrategies(new ExclusionStrategy() { + .setExclusionStrategies(new ExclusionStrategy() { - @Override - public boolean shouldSkipField(FieldAttributes f) { + @Override + public boolean shouldSkipField(FieldAttributes f) { - return false; - } + return false; + } - @Override - public boolean shouldSkipClass(Class clazz) { - // skip the JPA binding wrapper - if (clazz.equals(BeanPropertyBindingResult.class)) { - return true; - } - return false; - } + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } + return false; + } - }) - .serializeNulls() - .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .create(); + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { @@ -78,7 +83,7 @@ protected void renderMergedOutputModel(Map model, HttpServletReq response.setContentType("application/jrd+json"); - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); if (code == null) { code = HttpStatus.OK; // default to 200 } diff --git a/openid-connect-server/src/main/java/org/mitre/discovery/web/DiscoveryEndpoint.java b/openid-connect-server/src/main/java/org/mitre/discovery/web/DiscoveryEndpoint.java index ad2caa831c..270a7649eb 100644 --- a/openid-connect-server/src/main/java/org/mitre/discovery/web/DiscoveryEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/discovery/web/DiscoveryEndpoint.java @@ -1,36 +1,49 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.discovery.web; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.mitre.discovery.util.WebfingerURLNormalizer; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.oauth2.model.PKCEAlgorithm; import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.oauth2.web.DeviceEndpoint; +import org.mitre.oauth2.web.IntrospectionEndpoint; +import org.mitre.oauth2.web.RevocationEndpoint; import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.service.UserInfoService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.web.DynamicClientRegistrationEndpoint; +import org.mitre.openid.connect.web.EndSessionEndpoint; +import org.mitre.openid.connect.web.JWKSetPublishingEndpoint; +import org.mitre.openid.connect.web.UserInfoEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @@ -46,16 +59,23 @@ import com.nimbusds.jose.JWSAlgorithm; /** - * + * * Handle OpenID Connect Discovery. - * + * * @author jricher * */ @Controller public class DiscoveryEndpoint { - private static Logger logger = LoggerFactory.getLogger(DiscoveryEndpoint.class); + public static final String WELL_KNOWN_URL = ".well-known"; + public static final String OPENID_CONFIGURATION_URL = WELL_KNOWN_URL + "/openid-configuration"; + public static final String WEBFINGER_URL = WELL_KNOWN_URL + "/webfinger"; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DiscoveryEndpoint.class); @Autowired private ConfigurationPropertiesBean config; @@ -64,10 +84,10 @@ public class DiscoveryEndpoint { private SystemScopeService scopeService; @Autowired - private JwtSigningAndValidationService signService; + private JWTSigningAndValidationService signService; @Autowired - private JwtEncryptionAndDecryptionService encService; + private JWTEncryptionAndDecryptionService encService; @Autowired private UserInfoService userService; @@ -85,9 +105,12 @@ public String apply(Algorithm alg) { } }; - @RequestMapping(value={"/.well-known/webfinger"}, - params={"resource", "rel=http://openid.net/specs/connect/1.0/issuer"}, produces = "application/json") - public String webfinger(@RequestParam("resource") String resource, Model model) { + @RequestMapping(value={"/" + WEBFINGER_URL}, produces = MediaType.APPLICATION_JSON_VALUE) + public String webfinger(@RequestParam("resource") String resource, @RequestParam(value = "rel", required = false) String rel, Model model) { + + if (!Strings.isNullOrEmpty(rel) && !rel.equals("http://openid.net/specs/connect/1.0/issuer")) { + logger.warn("Responding to webfinger request for non-OIDC relation: " + rel); + } if (!resource.equals(config.getIssuer())) { // it's not the issuer directly, need to check other methods @@ -96,40 +119,52 @@ public String webfinger(@RequestParam("resource") String resource, Model model) if (resourceUri != null && resourceUri.getScheme() != null && resourceUri.getScheme().equals("acct")) { - // acct: URI + // acct: URI (email address format) - UserInfo user = userService.getByUsername(resourceUri.getUserInfo()); // first part is the username + // check on email addresses first + UserInfo user = userService.getByEmailAddress(resourceUri.getUserInfo() + "@" + resourceUri.getHost()); if (user == null) { - logger.info("User not found: " + resource); - model.addAttribute("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; - } + // user wasn't found, see if the local part of the username matches, plus our issuer host - UriComponents issuerComponents = UriComponentsBuilder.fromHttpUrl(config.getIssuer()).build(); - if (!Strings.nullToEmpty(issuerComponents.getHost()) - .equals(Strings.nullToEmpty(resourceUri.getHost()))) { - logger.info("Host mismatch, expected " + issuerComponents.getHost() + " got " + resourceUri.getHost()); - model.addAttribute("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; - } + user = userService.getByUsername(resourceUri.getUserInfo()); // first part is the username + + if (user != null) { + // username matched, check the host component + UriComponents issuerComponents = UriComponentsBuilder.fromHttpUrl(config.getIssuer()).build(); + if (!Strings.nullToEmpty(issuerComponents.getHost()) + .equals(Strings.nullToEmpty(resourceUri.getHost()))) { + logger.info("Host mismatch, expected " + issuerComponents.getHost() + " got " + resourceUri.getHost()); + model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + } else { + + // if the user's still null, punt and say we didn't find them + logger.info("User not found: " + resource); + model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + } } else { logger.info("Unknown URI format: " + resource); - model.addAttribute("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; + model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; } } - // if we got here, then we're good + // if we got here, then we're good, return ourselves model.addAttribute("resource", resource); model.addAttribute("issuer", config.getIssuer()); return "webfingerView"; } - @RequestMapping("/.well-known/openid-configuration") + @RequestMapping("/" + OPENID_CONFIGURATION_URL) public String providerConfiguration(Model model) { /* @@ -254,27 +289,35 @@ OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values String baseUrl = config.getIssuer(); if (!baseUrl.endsWith("/")) { - logger.warn("Configured issuer doesn't end in /, adding for discovery: " + baseUrl); + logger.debug("Configured issuer doesn't end in /, adding for discovery: {}", baseUrl); baseUrl = baseUrl.concat("/"); } - Collection serverSigningAlgs = signService.getAllSigningAlgsSupported(); - Collection clientSymmetricSigningAlgs = Lists.newArrayList(JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512); - Collection clientSymmetricAndAsymmetricSigningAlgs = Lists.newArrayList(JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512, JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512); - Collection clientSymmetricAndAsymmetricSigningAlgsWithNone = Lists.newArrayList(JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512, JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512, JWSAlgorithm.NONE); + signService.getAllSigningAlgsSupported(); + Lists.newArrayList(JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512); + Collection clientSymmetricAndAsymmetricSigningAlgs = Lists.newArrayList(JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512, + JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512, + JWSAlgorithm.ES256, JWSAlgorithm.ES384, JWSAlgorithm.ES512, + JWSAlgorithm.PS256, JWSAlgorithm.PS384, JWSAlgorithm.PS512); + Collection clientSymmetricAndAsymmetricSigningAlgsWithNone = Lists.newArrayList(JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512, + JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512, + JWSAlgorithm.ES256, JWSAlgorithm.ES384, JWSAlgorithm.ES512, + JWSAlgorithm.PS256, JWSAlgorithm.PS384, JWSAlgorithm.PS512, + Algorithm.NONE); + ArrayList grantTypes = Lists.newArrayList("authorization_code", "implicit", "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_credentials", "urn:ietf:params:oauth:grant_type:redelegate", "urn:ietf:params:oauth:grant-type:device_code","refresh_token"); - Map m = new HashMap(); + Map m = new HashMap<>(); m.put("issuer", config.getIssuer()); m.put("authorization_endpoint", baseUrl + "authorize"); m.put("token_endpoint", baseUrl + "token"); - m.put("userinfo_endpoint", baseUrl + "userinfo"); + m.put("userinfo_endpoint", baseUrl + UserInfoEndpoint.URL); //check_session_iframe - //end_session_endpoint - m.put("jwks_uri", baseUrl + "jwk"); - m.put("registration_endpoint", baseUrl + "register"); - m.put("scopes_supported", scopeService.toStrings(scopeService.getDynReg())); // these are the scopes that you can dynamically register for, which is what matters for discovery + m.put("end_session_endpoint", baseUrl + EndSessionEndpoint.URL); + m.put("jwks_uri", baseUrl + JWKSetPublishingEndpoint.URL); + m.put("registration_endpoint", baseUrl + DynamicClientRegistrationEndpoint.URL); + m.put("scopes_supported", scopeService.toStrings(scopeService.getUnrestricted())); // these are the scopes that you can dynamically register for, which is what matters for discovery m.put("response_types_supported", Lists.newArrayList("code", "token")); // we don't support these yet: , "id_token", "id_token token")); - m.put("grant_types_supported", Lists.newArrayList("authorization_code", "implicit", "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_credentials", "urn:ietf:params:oauth:grant_type:redelegate")); + m.put("grant_types_supported", grantTypes); //acr_values_supported m.put("subject_types_supported", Lists.newArrayList("public", "pairwise")); m.put("userinfo_signing_alg_values_supported", Collections2.transform(clientSymmetricAndAsymmetricSigningAlgs, toAlgorithmName)); @@ -302,13 +345,14 @@ OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values "picture", "website", "gender", - "zone_info", + "zoneinfo", "locale", - "updated_time", + "updated_at", "birthdate", "email", "email_verified", "phone_number", + "phone_number_verified", "address" )); m.put("service_documentation", baseUrl + "about"); @@ -321,12 +365,16 @@ OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values m.put("op_policy_uri", baseUrl + "about"); m.put("op_tos_uri", baseUrl + "about"); - m.put("introspection_endpoint", baseUrl + "introspect"); // token introspection endpoint for verifying tokens - m.put("revocation_endpoint", baseUrl + "revoke"); // token revocation endpoint + m.put("introspection_endpoint", baseUrl + IntrospectionEndpoint.URL); // token introspection endpoint for verifying tokens + m.put("revocation_endpoint", baseUrl + RevocationEndpoint.URL); // token revocation endpoint + + m.put("code_challenge_methods_supported", Lists.newArrayList(PKCEAlgorithm.plain.getName(), PKCEAlgorithm.S256.getName())); + + m.put("device_authorization_endpoint", baseUrl + DeviceEndpoint.URL); - model.addAttribute("entity", m); + model.addAttribute(JsonEntityView.ENTITY, m); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionAuthorizer.java b/openid-connect-server/src/main/java/org/mitre/oauth2/assertion/AssertionOAuth2RequestFactory.java old mode 100755 new mode 100644 similarity index 53% rename from openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionAuthorizer.java rename to openid-connect-server/src/main/java/org/mitre/oauth2/assertion/AssertionOAuth2RequestFactory.java index 66b72b7a7a..e8c9465ff3 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionAuthorizer.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/assertion/AssertionOAuth2RequestFactory.java @@ -1,36 +1,41 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * * 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 org.mitre.oauth2.service; + *******************************************************************************/ -import java.util.Set; +package org.mitre.oauth2.assertion; import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.TokenRequest; + +import com.nimbusds.jwt.JWT; /** - * Strategy interface used for authorizing token introspection. + * Take in an assertion and token request and generate an OAuth2Request from it, including scopes and other important components + * + * @author jricher + * */ -public interface IntrospectionAuthorizer { +public interface AssertionOAuth2RequestFactory { /** - * @param authClient the authenticated client wanting to perform token introspection - * @param tokenClient the client the token was issued to - * @param tokenScope the scope associated with the token - * @return {@code true} in case introspection is permitted; {@code false} otherwise + * @param client + * @param tokenRequest + * @param assertion + * @return */ - boolean isIntrospectionPermitted(ClientDetails authClient, ClientDetails tokenClient, Set tokenScope); + OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest, JWT assertion); } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/assertion/impl/DirectCopyRequestFactory.java b/openid-connect-server/src/main/java/org/mitre/oauth2/assertion/impl/DirectCopyRequestFactory.java new file mode 100644 index 0000000000..1b508dac4b --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/assertion/impl/DirectCopyRequestFactory.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.assertion.impl; + +import java.text.ParseException; +import java.util.Set; + +import org.mitre.oauth2.assertion.AssertionOAuth2RequestFactory; +import org.springframework.security.oauth2.common.util.OAuth2Utils; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.TokenRequest; + +import com.google.common.collect.Sets; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; + +/** + * Takes an assertion from a trusted source, looks for the fields: + * + * - scope, space-separated list of strings + * - aud, array of audience IDs + * + * @author jricher + * + */ +public class DirectCopyRequestFactory implements AssertionOAuth2RequestFactory { + + /* (non-Javadoc) + * @see org.mitre.oauth2.assertion.AssertionOAuth2RequestFactory#createOAuth2Request(org.springframework.security.oauth2.provider.ClientDetails, org.springframework.security.oauth2.provider.TokenRequest, com.nimbusds.jwt.JWT) + */ + @Override + public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest, JWT assertion) { + + try { + JWTClaimsSet claims = assertion.getJWTClaimsSet(); + Set scope = OAuth2Utils.parseParameterList(claims.getStringClaim("scope")); + + Set resources = Sets.newHashSet(claims.getAudience()); + + return new OAuth2Request(tokenRequest.getRequestParameters(), client.getClientId(), client.getAuthorities(), true, scope, resources, null, null, null); + } catch (ParseException e) { + return null; + } + + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/exception/AuthorizationPendingException.java b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/AuthorizationPendingException.java new file mode 100644 index 0000000000..c98f95cfc0 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/AuthorizationPendingException.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.exception; + +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + * @author jricher + * + */ +public class AuthorizationPendingException extends OAuth2Exception { + + /** + * @param msg + */ + public AuthorizationPendingException(String msg) { + super(msg); + } + + /** + * + */ + private static final long serialVersionUID = -7078098692596870940L; + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.exceptions.OAuth2Exception#getOAuth2ErrorCode() + */ + @Override + public String getOAuth2ErrorCode() { + return "authorization_pending"; + } + + + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/exception/DeviceCodeExpiredException.java b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/DeviceCodeExpiredException.java new file mode 100644 index 0000000000..3194531f82 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/DeviceCodeExpiredException.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.exception; + +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + * @author jricher + * + */ +public class DeviceCodeExpiredException extends OAuth2Exception { + + /** + * @param msg + */ + public DeviceCodeExpiredException(String msg) { + super(msg); + } + + /** + * + */ + private static final long serialVersionUID = -7078098692596870940L; + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.exceptions.OAuth2Exception#getOAuth2ErrorCode() + */ + @Override + public String getOAuth2ErrorCode() { + return "expired_token"; + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java index 1a4572aba5..52e90477e1 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.exception; public class DuplicateClientIdException extends RuntimeException { @@ -23,7 +24,7 @@ public DuplicateClientIdException(String clientId) { } /** - * + * */ private static final long serialVersionUID = 1L; diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthenticationHolderRepository.java b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthenticationHolderRepository.java index afb4e17798..269db62171 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthenticationHolderRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthenticationHolderRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository.impl; import java.util.List; @@ -22,47 +23,36 @@ import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; +import org.mitre.data.DefaultPageCriteria; +import org.mitre.data.PageCriteria; import org.mitre.oauth2.model.AuthenticationHolderEntity; import org.mitre.oauth2.repository.AuthenticationHolderRepository; import org.mitre.util.jpa.JpaUtil; -import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @Repository -@Transactional +@Transactional(value="defaultTransactionManager") public class JpaAuthenticationHolderRepository implements AuthenticationHolderRepository { private static final int MAXEXPIREDRESULTS = 1000; - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; @Override - public AuthenticationHolderEntity getById(Long id) { - return manager.find(AuthenticationHolderEntity.class, id); + public List getAll() { + TypedQuery query = manager.createNamedQuery(AuthenticationHolderEntity.QUERY_ALL, AuthenticationHolderEntity.class); + return query.getResultList(); } @Override - public AuthenticationHolderEntity getByAuthentication(OAuth2Authentication a) { - TypedQuery query = manager.createNamedQuery("AuthenticationHolderEntity.getByAuthentication", AuthenticationHolderEntity.class); - query.setParameter("authentication", a); - return JpaUtil.getSingleResult(query.getResultList()); - } - - @Override - @Transactional - public void removeById(Long id) { - AuthenticationHolderEntity found = getById(id); - if (found != null) { - manager.remove(found); - } else { - throw new IllegalArgumentException("AuthenticationHolderEntity not found: " + id); - } + public AuthenticationHolderEntity getById(Long id) { + return manager.find(AuthenticationHolderEntity.class, id); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void remove(AuthenticationHolderEntity a) { AuthenticationHolderEntity found = getById(a.getId()); if (found != null) { @@ -73,18 +63,23 @@ public void remove(AuthenticationHolderEntity a) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public AuthenticationHolderEntity save(AuthenticationHolderEntity a) { return JpaUtil.saveOrUpdate(a.getId(), manager, a); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public List getOrphanedAuthenticationHolders() { - TypedQuery query = manager.createNamedQuery("AuthenticationHolderEntity.getUnusedAuthenticationHolders", AuthenticationHolderEntity.class); - query.setMaxResults(MAXEXPIREDRESULTS); - List unusedAuthenticationHolders = query.getResultList(); - return unusedAuthenticationHolders; + DefaultPageCriteria pageCriteria = new DefaultPageCriteria(0,MAXEXPIREDRESULTS); + return getOrphanedAuthenticationHolders(pageCriteria); + } + + @Override + @Transactional(value="defaultTransactionManager") + public List getOrphanedAuthenticationHolders(PageCriteria pageCriteria) { + TypedQuery query = manager.createNamedQuery(AuthenticationHolderEntity.QUERY_GET_UNUSED, AuthenticationHolderEntity.class); + return JpaUtil.getResultPage(query, pageCriteria); } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthorizationCodeRepository.java b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthorizationCodeRepository.java index e5dd29380e..ad7788b6c0 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthorizationCodeRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaAuthorizationCodeRepository.java @@ -1,54 +1,57 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository.impl; +import java.util.Collection; +import java.util.Date; + import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; +import org.mitre.data.PageCriteria; import org.mitre.oauth2.model.AuthorizationCodeEntity; import org.mitre.oauth2.repository.AuthorizationCodeRepository; import org.mitre.util.jpa.JpaUtil; -import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; -import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; /** * JPA AuthorizationCodeRepository implementation. - * + * * @author aanganes * */ @Repository -@Transactional +@Transactional(value="defaultTransactionManager") public class JpaAuthorizationCodeRepository implements AuthorizationCodeRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") EntityManager manager; /* (non-Javadoc) * @see org.mitre.oauth2.repository.AuthorizationCodeRepository#save(org.mitre.oauth2.model.AuthorizationCodeEntity) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public AuthorizationCodeEntity save(AuthorizationCodeEntity authorizationCode) { return JpaUtil.saveOrUpdate(authorizationCode.getId(), manager, authorizationCode); @@ -56,27 +59,47 @@ public AuthorizationCodeEntity save(AuthorizationCodeEntity authorizationCode) { } /* (non-Javadoc) - * @see org.mitre.oauth2.repository.AuthorizationCodeRepository#consume(java.lang.String) + * @see org.mitre.oauth2.repository.AuthorizationCodeRepository#getByCode(java.lang.String) */ @Override - @Transactional - public OAuth2Authentication consume(String code) throws InvalidGrantException { - - TypedQuery query = manager.createNamedQuery("AuthorizationCodeEntity.getByValue", AuthorizationCodeEntity.class); + @Transactional(value="defaultTransactionManager") + public AuthorizationCodeEntity getByCode(String code) { + TypedQuery query = manager.createNamedQuery(AuthorizationCodeEntity.QUERY_BY_VALUE, AuthorizationCodeEntity.class); query.setParameter("code", code); AuthorizationCodeEntity result = JpaUtil.getSingleResult(query.getResultList()); + return result; + } - if (result == null) { - throw new InvalidGrantException("JpaAuthorizationCodeRepository: no authorization code found for value " + code); + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.AuthorizationCodeRepository#remove(org.mitre.oauth2.model.AuthorizationCodeEntity) + */ + @Override + public void remove(AuthorizationCodeEntity authorizationCodeEntity) { + AuthorizationCodeEntity found = manager.find(AuthorizationCodeEntity.class, authorizationCodeEntity.getId()); + if (found != null) { + manager.remove(found); } + } - OAuth2Authentication authRequest = result.getAuthentication(); - - manager.remove(result); + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.AuthorizationCodeRepository#getExpiredCodes() + */ + @Override + public Collection getExpiredCodes() { + TypedQuery query = manager.createNamedQuery(AuthorizationCodeEntity.QUERY_EXPIRATION_BY_DATE, AuthorizationCodeEntity.class); + query.setParameter(AuthorizationCodeEntity.PARAM_DATE, new Date()); // this gets anything that's already expired + return query.getResultList(); + } - return authRequest; + @Override + public Collection getExpiredCodes(PageCriteria pageCriteria) { + TypedQuery query = manager.createNamedQuery(AuthorizationCodeEntity.QUERY_EXPIRATION_BY_DATE, AuthorizationCodeEntity.class); + query.setParameter(AuthorizationCodeEntity.PARAM_DATE, new Date()); // this gets anything that's already expired + return JpaUtil.getResultPage(query, pageCriteria); } + + } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaDeviceCodeRepository.java b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaDeviceCodeRepository.java new file mode 100644 index 0000000000..65b1f03b2f --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaDeviceCodeRepository.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.repository.impl; + +import static org.mitre.util.jpa.JpaUtil.getSingleResult; +import static org.mitre.util.jpa.JpaUtil.saveOrUpdate; + +import java.util.Collection; +import java.util.Date; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; + +import org.mitre.oauth2.model.DeviceCode; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author jricher + * + */ +@Repository("jpaDeviceCodeRepository") +public class JpaDeviceCodeRepository implements DeviceCodeRepository { + + @PersistenceContext(unitName="defaultPersistenceUnit") + private EntityManager em; + + /* (non-Javadoc) + */ + @Override + @Transactional(value="defaultTransactionManager") + public DeviceCode getById(Long id) { + return em.find(DeviceCode.class, id); + } + + /* (non-Javadoc) + */ + @Override + @Transactional(value="defaultTransactionManager") + public DeviceCode getByUserCode(String value) { + TypedQuery query = em.createNamedQuery(DeviceCode.QUERY_BY_USER_CODE, DeviceCode.class); + query.setParameter(DeviceCode.PARAM_USER_CODE, value); + return getSingleResult(query.getResultList()); + } + + /* (non-Javadoc) + */ + @Override + @Transactional(value="defaultTransactionManager") + public DeviceCode getByDeviceCode(String value) { + TypedQuery query = em.createNamedQuery(DeviceCode.QUERY_BY_DEVICE_CODE, DeviceCode.class); + query.setParameter(DeviceCode.PARAM_DEVICE_CODE, value); + return getSingleResult(query.getResultList()); + } + + /* (non-Javadoc) + */ + @Override + @Transactional(value="defaultTransactionManager") + public void remove(DeviceCode scope) { + DeviceCode found = getById(scope.getId()); + + if (found != null) { + em.remove(found); + } + + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.SystemScopeRepository#save(org.mitre.oauth2.model.SystemScope) + */ + @Override + @Transactional(value="defaultTransactionManager") + public DeviceCode save(DeviceCode scope) { + return saveOrUpdate(scope.getId(), em, scope); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.impl.DeviceCodeRepository#getExpiredCodes() + */ + @Override + @Transactional(value="defaultTransactionManager") + public Collection getExpiredCodes() { + TypedQuery query = em.createNamedQuery(DeviceCode.QUERY_EXPIRED_BY_DATE, DeviceCode.class); + query.setParameter(DeviceCode.PARAM_DATE, new Date()); + return query.getResultList(); + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java index 139db6dfe4..58c0bf196f 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository.impl; import java.util.Collection; @@ -33,10 +34,10 @@ * */ @Repository -@Transactional +@Transactional(value="defaultTransactionManager") public class JpaOAuth2ClientRepository implements OAuth2ClientRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; public JpaOAuth2ClientRepository() { @@ -57,8 +58,8 @@ public ClientDetailsEntity getById(Long id) { */ @Override public ClientDetailsEntity getClientByClientId(String clientId) { - TypedQuery query = manager.createNamedQuery("ClientDetailsEntity.getByClientId", ClientDetailsEntity.class); - query.setParameter("clientId", clientId); + TypedQuery query = manager.createNamedQuery(ClientDetailsEntity.QUERY_BY_CLIENT_ID, ClientDetailsEntity.class); + query.setParameter(ClientDetailsEntity.PARAM_CLIENT_ID, clientId); return JpaUtil.getSingleResult(query.getResultList()); } @@ -93,7 +94,7 @@ public ClientDetailsEntity updateClient(Long id, ClientDetailsEntity client) { @Override public Collection getAllClients() { - TypedQuery query = manager.createNamedQuery("ClientDetailsEntity.findAll", ClientDetailsEntity.class); + TypedQuery query = manager.createNamedQuery(ClientDetailsEntity.QUERY_ALL, ClientDetailsEntity.class); return query.getResultList(); } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java index 2cd217fda5..718a233572 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java @@ -1,65 +1,88 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository.impl; +import java.text.ParseException; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import javax.persistence.Query; import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaDelete; +import javax.persistence.criteria.Root; +import org.mitre.data.DefaultPageCriteria; +import org.mitre.data.PageCriteria; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.uma.model.ResourceSet; import org.mitre.util.jpa.JpaUtil; -import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + @Repository public class JpaOAuth2TokenRepository implements OAuth2TokenRepository { private static final int MAXEXPIREDRESULTS = 1000; - @PersistenceContext + private static final Logger logger = LoggerFactory.getLogger(JpaOAuth2TokenRepository.class); + + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; @Override public Set getAllAccessTokens() { - TypedQuery query = manager.createNamedQuery("OAuth2AccessTokenEntity.getAll", OAuth2AccessTokenEntity.class); - return new LinkedHashSet(query.getResultList()); + TypedQuery query = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_ALL, OAuth2AccessTokenEntity.class); + return new LinkedHashSet<>(query.getResultList()); } @Override public Set getAllRefreshTokens() { - TypedQuery query = manager.createNamedQuery("OAuth2RefreshTokenEntity.getAll", OAuth2RefreshTokenEntity.class); - return new LinkedHashSet(query.getResultList()); + TypedQuery query = manager.createNamedQuery(OAuth2RefreshTokenEntity.QUERY_ALL, OAuth2RefreshTokenEntity.class); + return new LinkedHashSet<>(query.getResultList()); } @Override public OAuth2AccessTokenEntity getAccessTokenByValue(String accessTokenValue) { - TypedQuery query = manager.createNamedQuery("OAuth2AccessTokenEntity.getByTokenValue", OAuth2AccessTokenEntity.class); - query.setParameter("tokenValue", accessTokenValue); - return JpaUtil.getSingleResult(query.getResultList()); + try { + JWT jwt = JWTParser.parse(accessTokenValue); + TypedQuery query = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_BY_TOKEN_VALUE, OAuth2AccessTokenEntity.class); + query.setParameter(OAuth2AccessTokenEntity.PARAM_TOKEN_VALUE, jwt); + return JpaUtil.getSingleResult(query.getResultList()); + } catch (ParseException e) { + return null; + } } @Override @@ -68,15 +91,15 @@ public OAuth2AccessTokenEntity getAccessTokenById(Long id) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public OAuth2AccessTokenEntity saveAccessToken(OAuth2AccessTokenEntity token) { return JpaUtil.saveOrUpdate(token.getId(), manager, token); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void removeAccessToken(OAuth2AccessTokenEntity accessToken) { - OAuth2AccessTokenEntity found = getAccessTokenByValue(accessToken.getValue()); + OAuth2AccessTokenEntity found = getAccessTokenById(accessToken.getId()); if (found != null) { manager.remove(found); } else { @@ -85,10 +108,10 @@ public void removeAccessToken(OAuth2AccessTokenEntity accessToken) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void clearAccessTokensForRefreshToken(OAuth2RefreshTokenEntity refreshToken) { - TypedQuery query = manager.createNamedQuery("OAuth2AccessTokenEntity.getByRefreshToken", OAuth2AccessTokenEntity.class); - query.setParameter("refreshToken", refreshToken); + TypedQuery query = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_BY_REFRESH_TOKEN, OAuth2AccessTokenEntity.class); + query.setParameter(OAuth2AccessTokenEntity.PARAM_REFERSH_TOKEN, refreshToken); List accessTokens = query.getResultList(); for (OAuth2AccessTokenEntity accessToken : accessTokens) { removeAccessToken(accessToken); @@ -97,9 +120,14 @@ public void clearAccessTokensForRefreshToken(OAuth2RefreshTokenEntity refreshTok @Override public OAuth2RefreshTokenEntity getRefreshTokenByValue(String refreshTokenValue) { - TypedQuery query = manager.createNamedQuery("OAuth2RefreshTokenEntity.getByTokenValue", OAuth2RefreshTokenEntity.class); - query.setParameter("tokenValue", refreshTokenValue); - return JpaUtil.getSingleResult(query.getResultList()); + try { + JWT jwt = JWTParser.parse(refreshTokenValue); + TypedQuery query = manager.createNamedQuery(OAuth2RefreshTokenEntity.QUERY_BY_TOKEN_VALUE, OAuth2RefreshTokenEntity.class); + query.setParameter(OAuth2RefreshTokenEntity.PARAM_TOKEN_VALUE, jwt); + return JpaUtil.getSingleResult(query.getResultList()); + } catch (ParseException e) { + return null; + } } @Override @@ -108,15 +136,15 @@ public OAuth2RefreshTokenEntity getRefreshTokenById(Long id) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public OAuth2RefreshTokenEntity saveRefreshToken(OAuth2RefreshTokenEntity refreshToken) { return JpaUtil.saveOrUpdate(refreshToken.getId(), manager, refreshToken); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void removeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { - OAuth2RefreshTokenEntity found = getRefreshTokenByValue(refreshToken.getValue()); + OAuth2RefreshTokenEntity found = getRefreshTokenById(refreshToken.getId()); if (found != null) { manager.remove(found); } else { @@ -125,77 +153,136 @@ public void removeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void clearTokensForClient(ClientDetailsEntity client) { - TypedQuery queryA = manager.createNamedQuery("OAuth2AccessTokenEntity.getByClient", OAuth2AccessTokenEntity.class); - queryA.setParameter("client", client); + TypedQuery queryA = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_BY_CLIENT, OAuth2AccessTokenEntity.class); + queryA.setParameter(OAuth2AccessTokenEntity.PARAM_CLIENT, client); List accessTokens = queryA.getResultList(); for (OAuth2AccessTokenEntity accessToken : accessTokens) { removeAccessToken(accessToken); } - TypedQuery queryR = manager.createNamedQuery("OAuth2RefreshTokenEntity.getByClient", OAuth2RefreshTokenEntity.class); - queryR.setParameter("client", client); + TypedQuery queryR = manager.createNamedQuery(OAuth2RefreshTokenEntity.QUERY_BY_CLIENT, OAuth2RefreshTokenEntity.class); + queryR.setParameter(OAuth2RefreshTokenEntity.PARAM_CLIENT, client); List refreshTokens = queryR.getResultList(); for (OAuth2RefreshTokenEntity refreshToken : refreshTokens) { removeRefreshToken(refreshToken); } } - /* (non-Javadoc) - * @see org.mitre.oauth2.repository.OAuth2TokenRepository#getAccessTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) - */ @Override public List getAccessTokensForClient(ClientDetailsEntity client) { - TypedQuery queryA = manager.createNamedQuery("OAuth2AccessTokenEntity.getByClient", OAuth2AccessTokenEntity.class); - queryA.setParameter("client", client); + TypedQuery queryA = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_BY_CLIENT, OAuth2AccessTokenEntity.class); + queryA.setParameter(OAuth2AccessTokenEntity.PARAM_CLIENT, client); List accessTokens = queryA.getResultList(); return accessTokens; } - /* (non-Javadoc) - * @see org.mitre.oauth2.repository.OAuth2TokenRepository#getRefreshTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) - */ @Override public List getRefreshTokensForClient(ClientDetailsEntity client) { - TypedQuery queryR = manager.createNamedQuery("OAuth2RefreshTokenEntity.getByClient", OAuth2RefreshTokenEntity.class); - queryR.setParameter("client", client); + TypedQuery queryR = manager.createNamedQuery(OAuth2RefreshTokenEntity.QUERY_BY_CLIENT, OAuth2RefreshTokenEntity.class); + queryR.setParameter(OAuth2RefreshTokenEntity.PARAM_CLIENT, client); List refreshTokens = queryR.getResultList(); return refreshTokens; } - + @Override - public OAuth2AccessTokenEntity getByAuthentication(OAuth2Authentication auth) { - TypedQuery queryA = manager.createNamedQuery("OAuth2AccessTokenEntity.getByAuthentication", OAuth2AccessTokenEntity.class); - queryA.setParameter("authentication", auth); - List accessTokens = queryA.getResultList(); - return JpaUtil.getSingleResult(accessTokens); + public Set getAccessTokensByUserName(String name) { + TypedQuery query = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_BY_NAME, OAuth2AccessTokenEntity.class); + query.setParameter(OAuth2AccessTokenEntity.PARAM_NAME, name); + List results = query.getResultList(); + return results != null ? new HashSet<>(results) : new HashSet<>(); } - - /* (non-Javadoc) - * @see org.mitre.oauth2.repository.OAuth2TokenRepository#getAccessTokenForIdToken(org.mitre.oauth2.model.OAuth2AccessTokenEntity) - */ + @Override - public OAuth2AccessTokenEntity getAccessTokenForIdToken(OAuth2AccessTokenEntity idToken) { - TypedQuery queryA = manager.createNamedQuery("OAuth2AccessTokenEntity.getByIdToken", OAuth2AccessTokenEntity.class); - queryA.setParameter("idToken", idToken); - List accessTokens = queryA.getResultList(); - return JpaUtil.getSingleResult(accessTokens); + public Set getRefreshTokensByUserName(String name) { + TypedQuery query = manager.createNamedQuery(OAuth2RefreshTokenEntity.QUERY_BY_NAME, OAuth2RefreshTokenEntity.class); + query.setParameter(OAuth2RefreshTokenEntity.PARAM_NAME, name); + List results = query.getResultList(); + return results != null ? new HashSet<>(results) : new HashSet<>(); } @Override public Set getAllExpiredAccessTokens() { - TypedQuery query = manager.createNamedQuery("OAuth2AccessTokenEntity.getAllExpiredByDate", OAuth2AccessTokenEntity.class); - query.setParameter("date", new Date()); - query.setMaxResults(MAXEXPIREDRESULTS); - return new LinkedHashSet(query.getResultList()); + DefaultPageCriteria pageCriteria = new DefaultPageCriteria(0, MAXEXPIREDRESULTS); + return getAllExpiredAccessTokens(pageCriteria); + } + + @Override + public Set getAllExpiredAccessTokens(PageCriteria pageCriteria) { + TypedQuery query = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_EXPIRED_BY_DATE, OAuth2AccessTokenEntity.class); + query.setParameter(OAuth2AccessTokenEntity.PARAM_DATE, new Date()); + return new LinkedHashSet<>(JpaUtil.getResultPage(query, pageCriteria)); } @Override public Set getAllExpiredRefreshTokens() { - TypedQuery query = manager.createNamedQuery("OAuth2RefreshTokenEntity.getAllExpiredByDate", OAuth2RefreshTokenEntity.class); - query.setParameter("date", new Date()); - query.setMaxResults(MAXEXPIREDRESULTS); - return new LinkedHashSet(query.getResultList()); + DefaultPageCriteria pageCriteria = new DefaultPageCriteria(0, MAXEXPIREDRESULTS); + return getAllExpiredRefreshTokens(pageCriteria); + } + + @Override + public Set getAllExpiredRefreshTokens(PageCriteria pageCriteria) { + TypedQuery query = manager.createNamedQuery(OAuth2RefreshTokenEntity.QUERY_EXPIRED_BY_DATE, OAuth2RefreshTokenEntity.class); + query.setParameter(OAuth2AccessTokenEntity.PARAM_DATE, new Date()); + return new LinkedHashSet<>(JpaUtil.getResultPage(query,pageCriteria)); + } + + @Override + public Set getAccessTokensForResourceSet(ResourceSet rs) { + TypedQuery query = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_BY_RESOURCE_SET, OAuth2AccessTokenEntity.class); + query.setParameter(OAuth2AccessTokenEntity.PARAM_RESOURCE_SET_ID, rs.getId()); + return new LinkedHashSet<>(query.getResultList()); + } + + @Override + @Transactional(value="defaultTransactionManager") + public void clearDuplicateAccessTokens() { + Query query = manager.createQuery("select a.jwt, count(1) as c from OAuth2AccessTokenEntity a GROUP BY a.jwt HAVING count(1) > 1"); + @SuppressWarnings("unchecked") + List resultList = query.getResultList(); + List values = new ArrayList<>(); + for (Object[] r : resultList) { + logger.warn("Found duplicate access tokens: {}, {}", ((JWT)r[0]).serialize(), r[1]); + values.add((JWT) r[0]); + } + if (values.size() > 0) { + CriteriaBuilder cb = manager.getCriteriaBuilder(); + CriteriaDelete criteriaDelete = cb.createCriteriaDelete(OAuth2AccessTokenEntity.class); + Root root = criteriaDelete.from(OAuth2AccessTokenEntity.class); + criteriaDelete.where(root.get("jwt").in(values)); + int result = manager.createQuery(criteriaDelete).executeUpdate(); + logger.warn("Deleted {} duplicate access tokens", result); + } + } + + @Override + @Transactional(value="defaultTransactionManager") + public void clearDuplicateRefreshTokens() { + Query query = manager.createQuery("select a.jwt, count(1) as c from OAuth2RefreshTokenEntity a GROUP BY a.jwt HAVING count(1) > 1"); + @SuppressWarnings("unchecked") + List resultList = query.getResultList(); + List values = new ArrayList<>(); + for (Object[] r : resultList) { + logger.warn("Found duplicate refresh tokens: {}, {}", ((JWT)r[0]).serialize(), r[1]); + values.add((JWT) r[0]); + } + if (values.size() > 0) { + CriteriaBuilder cb = manager.getCriteriaBuilder(); + CriteriaDelete criteriaDelete = cb.createCriteriaDelete(OAuth2RefreshTokenEntity.class); + Root root = criteriaDelete.from(OAuth2RefreshTokenEntity.class); + criteriaDelete.where(root.get("jwt").in(values)); + int result = manager.createQuery(criteriaDelete).executeUpdate(); + logger.warn("Deleted {} duplicate refresh tokens", result); + } + + } + + @Override + public List getAccessTokensForApprovedSite(ApprovedSite approvedSite) { + TypedQuery queryA = manager.createNamedQuery(OAuth2AccessTokenEntity.QUERY_BY_APPROVED_SITE, OAuth2AccessTokenEntity.class); + queryA.setParameter(OAuth2AccessTokenEntity.PARAM_APPROVED_SITE, approvedSite); + List accessTokens = queryA.getResultList(); + return accessTokens; } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaSystemScopeRepository.java b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaSystemScopeRepository.java index 8f27ac655d..f646b57243 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaSystemScopeRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/repository/impl/JpaSystemScopeRepository.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.repository.impl; @@ -41,25 +42,25 @@ @Repository("jpaSystemScopeRepository") public class JpaSystemScopeRepository implements SystemScopeRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager em; /* (non-Javadoc) * @see org.mitre.oauth2.repository.SystemScopeRepository#getAll() */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Set getAll() { - TypedQuery query = em.createNamedQuery("SystemScope.findAll", SystemScope.class); + TypedQuery query = em.createNamedQuery(SystemScope.QUERY_ALL, SystemScope.class); - return new LinkedHashSet(query.getResultList()); + return new LinkedHashSet<>(query.getResultList()); } /* (non-Javadoc) * @see org.mitre.oauth2.repository.SystemScopeRepository#getById(java.lang.Long) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public SystemScope getById(Long id) { return em.find(SystemScope.class, id); } @@ -68,10 +69,10 @@ public SystemScope getById(Long id) { * @see org.mitre.oauth2.repository.SystemScopeRepository#getByValue(java.lang.String) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public SystemScope getByValue(String value) { - TypedQuery query = em.createNamedQuery("SystemScope.getByValue", SystemScope.class); - query.setParameter("value", value); + TypedQuery query = em.createNamedQuery(SystemScope.QUERY_BY_VALUE, SystemScope.class); + query.setParameter(SystemScope.PARAM_VALUE, value); return getSingleResult(query.getResultList()); } @@ -79,7 +80,7 @@ public SystemScope getByValue(String value) { * @see org.mitre.oauth2.repository.SystemScopeRepository#remove(org.mitre.oauth2.model.SystemScope) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void remove(SystemScope scope) { SystemScope found = getById(scope.getId()); @@ -93,7 +94,7 @@ public void remove(SystemScope scope) { * @see org.mitre.oauth2.repository.SystemScopeRepository#save(org.mitre.oauth2.model.SystemScope) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public SystemScope save(SystemScope scope) { return saveOrUpdate(scope.getId(), em, scope); } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/BlacklistAwareRedirectResolver.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/BlacklistAwareRedirectResolver.java index e3b95c1fa5..fc45ed20b9 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/BlacklistAwareRedirectResolver.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/BlacklistAwareRedirectResolver.java @@ -1,18 +1,39 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service.impl; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.mitre.openid.connect.service.BlacklistedSiteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver; -import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; import org.springframework.stereotype.Component; +import com.google.common.base.Strings; + /** + * + * A redirect resolver that knows how to check against the blacklisted URIs + * for forbidden values. Can be configured to do strict string matching also. + * * @author jricher * */ @@ -21,7 +42,12 @@ public class BlacklistAwareRedirectResolver extends DefaultRedirectResolver { @Autowired private BlacklistedSiteService blacklistService; - + + @Autowired + private ConfigurationPropertiesBean config; + + private boolean strictMatch = true; + /* (non-Javadoc) * @see org.springframework.security.oauth2.provider.endpoint.RedirectResolver#resolveRedirect(java.lang.String, org.springframework.security.oauth2.provider.ClientDetails) */ @@ -37,4 +63,43 @@ public String resolveRedirect(String requestedRedirect, ClientDetails client) th } } + /* (non-Javadoc) + * @see org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver#redirectMatches(java.lang.String, java.lang.String) + */ + @Override + protected boolean redirectMatches(String requestedRedirect, String redirectUri) { + + if (isStrictMatch()) { + // we're doing a strict string match for all clients + return Strings.nullToEmpty(requestedRedirect).equals(redirectUri); + } else { + // otherwise do the prefix-match from the library + return super.redirectMatches(requestedRedirect, redirectUri); + } + + } + + /** + * @return the strictMatch + */ + public boolean isStrictMatch() { + if (config.isHeartMode()) { + // HEART mode enforces strict matching + return true; + } else { + return strictMatch; + } + } + + /** + * Set this to true to require exact string matches for all redirect URIs. (Default is false) + * + * @param strictMatch the strictMatch to set + */ + public void setStrictMatch(boolean strictMatch) { + this.strictMatch = strictMatch; + } + + + } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultDeviceCodeService.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultDeviceCodeService.java new file mode 100644 index 0000000000..29f4ec8d22 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultDeviceCodeService.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service.impl; + +import java.util.Collection; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.mitre.data.AbstractPageOperationTemplate; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.DeviceCode; +import org.mitre.oauth2.repository.impl.DeviceCodeRepository; +import org.mitre.oauth2.service.DeviceCodeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author jricher + * + */ +@Service("defaultDeviceCodeService") +public class DefaultDeviceCodeService implements DeviceCodeService { + + @Autowired + private DeviceCodeRepository repository; + + private RandomValueStringGenerator randomGenerator = new RandomValueStringGenerator(); + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.DeviceCodeService#save(org.mitre.oauth2.model.DeviceCode) + */ + @Override + public DeviceCode createNewDeviceCode(Set requestedScopes, ClientDetailsEntity client, Map parameters) { + + // create a device code, should be big and random + String deviceCode = UUID.randomUUID().toString(); + + // create a user code, should be random but small and typable, and always uppercase (lookup is case insensitive) + String userCode = randomGenerator.generate().toUpperCase(); + + DeviceCode dc = new DeviceCode(deviceCode, userCode, requestedScopes, client.getClientId(), parameters); + + if (client.getDeviceCodeValiditySeconds() != null) { + dc.setExpiration(new Date(System.currentTimeMillis() + client.getDeviceCodeValiditySeconds() * 1000L)); + } + + dc.setApproved(false); + + return repository.save(dc); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.DeviceCodeService#lookUpByUserCode(java.lang.String) + */ + @Override + public DeviceCode lookUpByUserCode(String userCode) { + // always up-case the code for lookup + return repository.getByUserCode(userCode.toUpperCase()); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.DeviceCodeService#approveDeviceCode(org.mitre.oauth2.model.DeviceCode) + */ + @Override + public DeviceCode approveDeviceCode(DeviceCode dc, OAuth2Authentication auth) { + DeviceCode found = repository.getById(dc.getId()); + + found.setApproved(true); + + AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); + authHolder.setAuthentication(auth); + + found.setAuthenticationHolder(authHolder); + + return repository.save(found); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.DeviceCodeService#consumeDeviceCode(java.lang.String, org.springframework.security.oauth2.provider.ClientDetails) + */ + @Override + public DeviceCode findDeviceCode(String deviceCode, ClientDetails client) { + DeviceCode found = repository.getByDeviceCode(deviceCode); + + if (found != null) { + if (found.getClientId().equals(client.getClientId())) { + // make sure the client matches, if so, we're good + return found; + } else { + // if the clients don't match, pretend the code wasn't found + return null; + } + } else { + // didn't find the code, return null + return null; + } + + } + + + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.DeviceCodeService#clearExpiredDeviceCodes() + */ + @Override + @Transactional(value="defaultTransactionManager") + public void clearExpiredDeviceCodes() { + + new AbstractPageOperationTemplate("clearExpiredDeviceCodes"){ + @Override + public Collection fetchPage() { + return repository.getExpiredCodes(); + } + + @Override + protected void doOperation(DeviceCode item) { + repository.remove(item); + } + }.execute(); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.DeviceCodeService#clearDeviceCode(java.lang.String, org.springframework.security.oauth2.provider.ClientDetails) + */ + @Override + public void clearDeviceCode(String deviceCode, ClientDetails client) { + DeviceCode found = findDeviceCode(deviceCode, client); + + if (found != null) { + // make sure it's not used twice + repository.remove(found); + } + + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionAuthorizer.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionAuthorizer.java deleted file mode 100755 index a281687fe4..0000000000 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionAuthorizer.java +++ /dev/null @@ -1,43 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.oauth2.service.impl; - -import java.util.Set; - -import org.mitre.oauth2.service.IntrospectionAuthorizer; -import org.mitre.oauth2.service.SystemScopeService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.stereotype.Service; - -@Service -public class DefaultIntrospectionAuthorizer implements IntrospectionAuthorizer { - - @Autowired - private SystemScopeService scopeService; - - @Override - public boolean isIntrospectionPermitted(ClientDetails authClient, - ClientDetails tokenClient, Set tokenScope) { - // permit introspection if it's the same client that the token was - // issued to, or it at least has all the scopes the token was issued - // with - return authClient.getClientId().equals(tokenClient.getClientId()) - || scopeService.scopesMatch(authClient.getScope(), tokenScope); - } - -} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionResultAssembler.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionResultAssembler.java new file mode 100644 index 0000000000..ea36949fb6 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionResultAssembler.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service.impl; + +import static com.google.common.collect.Maps.newLinkedHashMap; + +import java.text.ParseException; +import java.util.Map; +import java.util.Set; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.service.IntrospectionResultAssembler; +import org.mitre.openid.connect.model.UserInfo; +import org.mitre.uma.model.Permission; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Service; + +import com.google.common.base.Joiner; +import com.google.common.collect.Sets; + +/** + * Default implementation of the {@link IntrospectionResultAssembler} interface. + */ +@Service +public class DefaultIntrospectionResultAssembler implements IntrospectionResultAssembler { + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultIntrospectionResultAssembler.class); + + @Override + public Map assembleFrom(OAuth2AccessTokenEntity accessToken, UserInfo userInfo, Set authScopes) { + + Map result = newLinkedHashMap(); + OAuth2Authentication authentication = accessToken.getAuthenticationHolder().getAuthentication(); + + result.put(ACTIVE, true); + + if (accessToken.getPermissions() != null && !accessToken.getPermissions().isEmpty()) { + + Set permissions = Sets.newHashSet(); + + for (Permission perm : accessToken.getPermissions()) { + Map o = newLinkedHashMap(); + o.put("resource_set_id", perm.getResourceSet().getId().toString()); + Set scopes = Sets.newHashSet(perm.getScopes()); + o.put("scopes", scopes); + permissions.add(o); + } + + result.put("permissions", permissions); + + } else { + Set scopes = Sets.intersection(authScopes, accessToken.getScope()); + + result.put(SCOPE, Joiner.on(SCOPE_SEPARATOR).join(scopes)); + + } + + if (accessToken.getExpiration() != null) { + try { + result.put(EXPIRES_AT, dateFormat.valueToString(accessToken.getExpiration())); + result.put(EXP, accessToken.getExpiration().getTime() / 1000L); + } catch (ParseException e) { + logger.error("Parse exception in token introspection", e); + } + } + + if (userInfo != null) { + // if we have a UserInfo, use that for the subject + result.put(SUB, userInfo.getSub()); + } else { + // otherwise, use the authentication's username + result.put(SUB, authentication.getName()); + } + + if(authentication.getUserAuthentication() != null) { + result.put(USER_ID, authentication.getUserAuthentication().getName()); + } + + result.put(CLIENT_ID, authentication.getOAuth2Request().getClientId()); + + result.put(TOKEN_TYPE, accessToken.getTokenType()); + + return result; + } + + @Override + public Map assembleFrom(OAuth2RefreshTokenEntity refreshToken, UserInfo userInfo, Set authScopes) { + + Map result = newLinkedHashMap(); + OAuth2Authentication authentication = refreshToken.getAuthenticationHolder().getAuthentication(); + + result.put(ACTIVE, true); + + Set scopes = Sets.intersection(authScopes, authentication.getOAuth2Request().getScope()); + + result.put(SCOPE, Joiner.on(SCOPE_SEPARATOR).join(scopes)); + + if (refreshToken.getExpiration() != null) { + try { + result.put(EXPIRES_AT, dateFormat.valueToString(refreshToken.getExpiration())); + result.put(EXP, refreshToken.getExpiration().getTime() / 1000L); + } catch (ParseException e) { + logger.error("Parse exception in token introspection", e); + } + } + + + if (userInfo != null) { + // if we have a UserInfo, use that for the subject + result.put(SUB, userInfo.getSub()); + } else { + // otherwise, use the authentication's username + result.put(SUB, authentication.getName()); + } + + if(authentication.getUserAuthentication() != null) { + result.put(USER_ID, authentication.getUserAuthentication().getName()); + } + + result.put(CLIENT_ID, authentication.getOAuth2Request().getClientId()); + + return result; + } +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2AuthorizationCodeService.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2AuthorizationCodeService.java index 8a0a659900..b062a1a4ae 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2AuthorizationCodeService.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2AuthorizationCodeService.java @@ -1,60 +1,86 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; +import java.util.Collection; +import java.util.Date; + +import org.mitre.data.AbstractPageOperationTemplate; +import org.mitre.oauth2.model.AuthenticationHolderEntity; import org.mitre.oauth2.model.AuthorizationCodeEntity; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; import org.mitre.oauth2.repository.AuthorizationCodeRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; /** * Database-backed, random-value authorization code service implementation. - * + * * @author aanganes * */ -@Service +@Service("defaultOAuth2AuthorizationCodeService") public class DefaultOAuth2AuthorizationCodeService implements AuthorizationCodeServices { + // Logger for this class + private static final Logger logger = LoggerFactory.getLogger(DefaultOAuth2AuthorizationCodeService.class); @Autowired private AuthorizationCodeRepository repository; - private RandomValueStringGenerator generator = new RandomValueStringGenerator(); + @Autowired + private AuthenticationHolderRepository authenticationHolderRepository; + + private int authCodeExpirationSeconds = 60 * 5; // expire in 5 minutes by default + + private RandomValueStringGenerator generator = new RandomValueStringGenerator(22); /** * Generate a random authorization code and create an AuthorizationCodeEntity, * which will be stored in the repository. - * + * * @param authentication the authentication of the current user, to be retrieved when the * code is consumed * @return the authorization code */ @Override + @Transactional(value="defaultTransactionManager") public String createAuthorizationCode(OAuth2Authentication authentication) { String code = generator.generate(); - AuthorizationCodeEntity entity = new AuthorizationCodeEntity(code, authentication); + // attach the authorization so that we can look it up later + AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); + authHolder.setAuthentication(authentication); + authHolder = authenticationHolderRepository.save(authHolder); + + // set the auth code to expire + Date expiration = new Date(System.currentTimeMillis() + (getAuthCodeExpirationSeconds() * 1000L)); + + AuthorizationCodeEntity entity = new AuthorizationCodeEntity(code, authHolder, expiration); repository.save(entity); return code; @@ -65,7 +91,7 @@ public String createAuthorizationCode(OAuth2Authentication authentication) { * Match the provided string to an AuthorizationCodeEntity. If one is found, return * the authentication associated with the code. If one is not found, throw an * InvalidGrantException. - * + * * @param code the authorization code * @return the authentication that made the original request * @throws InvalidGrantException, if an AuthorizationCodeEntity is not found with the given value @@ -73,10 +99,38 @@ public String createAuthorizationCode(OAuth2Authentication authentication) { @Override public OAuth2Authentication consumeAuthorizationCode(String code) throws InvalidGrantException { - OAuth2Authentication auth = repository.consume(code); + AuthorizationCodeEntity result = repository.getByCode(code); + + if (result == null) { + throw new InvalidGrantException("JpaAuthorizationCodeRepository: no authorization code found for value " + code); + } + + OAuth2Authentication auth = result.getAuthenticationHolder().getAuthentication(); + + repository.remove(result); + return auth; } + /** + * Find and remove all expired auth codes. + */ + @Transactional(value="defaultTransactionManager") + public void clearExpiredAuthorizationCodes() { + + new AbstractPageOperationTemplate("clearExpiredAuthorizationCodes"){ + @Override + public Collection fetchPage() { + return repository.getExpiredCodes(); + } + + @Override + protected void doOperation(AuthorizationCodeEntity item) { + repository.remove(item); + } + }.execute(); + } + /** * @return the repository */ @@ -91,4 +145,18 @@ public void setRepository(AuthorizationCodeRepository repository) { this.repository = repository; } + /** + * @return the authCodeExpirationSeconds + */ + public int getAuthCodeExpirationSeconds() { + return authCodeExpirationSeconds; + } + + /** + * @param authCodeExpirationSeconds the authCodeExpirationSeconds to set + */ + public void setAuthCodeExpirationSeconds(int authCodeExpirationSeconds) { + this.authCodeExpirationSeconds = authCodeExpirationSeconds; + } + } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java index d74449ce3f..3416b84ea1 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; import java.math.BigInteger; @@ -22,23 +23,29 @@ import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.apache.commons.codec.binary.Base64; import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.repository.OAuth2ClientRepository; import org.mitre.oauth2.repository.OAuth2TokenRepository; import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.mitre.openid.connect.model.WhitelistedSite; import org.mitre.openid.connect.service.ApprovedSiteService; import org.mitre.openid.connect.service.BlacklistedSiteService; import org.mitre.openid.connect.service.StatsService; import org.mitre.openid.connect.service.WhitelistedSiteService; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ResourceSetService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -47,6 +54,8 @@ import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; import com.google.common.base.Strings; import com.google.common.cache.CacheBuilder; @@ -59,7 +68,10 @@ @Service public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEntityService { - private static Logger logger = LoggerFactory.getLogger(DefaultOAuth2ClientDetailsEntityService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultOAuth2ClientDetailsEntityService.class); @Autowired private OAuth2ClientRepository clientRepository; @@ -82,11 +94,17 @@ public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEnt @Autowired private StatsService statsService; + @Autowired + private ResourceSetService resourceSetService; + + @Autowired + private ConfigurationPropertiesBean config; + // map of sector URI -> list of redirect URIs private LoadingCache> sectorRedirects = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.HOURS) .maximumSize(100) - .build(new SectorIdentifierLoader()); + .build(new SectorIdentifierLoader(HttpClientBuilder.create().useSystemProperties().build())); @Override public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) { @@ -108,18 +126,60 @@ public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) { client = generateClientId(client); } - // if the client is flagged to allow for refresh tokens, make sure it's got the right granted scopes - if (client.isAllowRefresh()) { - client.getScope().add(SystemScopeService.OFFLINE_ACCESS); - } else { - client.getScope().remove(SystemScopeService.OFFLINE_ACCESS); - } + // make sure that clients with the "refresh_token" grant type have the "offline_access" scope, and vice versa + ensureRefreshTokenConsistency(client); + + // make sure we don't have both a JWKS and a JWKS URI + ensureKeyConsistency(client); + + // check consistency when using HEART mode + checkHeartMode(client); // timestamp this to right now client.setCreatedAt(new Date()); // check the sector URI + checkSectorIdentifierUri(client); + + + ensureNoReservedScopes(client); + + ClientDetailsEntity c = clientRepository.saveClient(client); + + statsService.resetCache(); + + return c; + } + + /** + * Make sure the client has only one type of key registered + * @param client + */ + private void ensureKeyConsistency(ClientDetailsEntity client) { + if (client.getJwksUri() != null && client.getJwks() != null) { + // a client can only have one key type or the other, not both + throw new IllegalArgumentException("A client cannot have both JWKS URI and JWKS value"); + } + } + + /** + * Make sure the client doesn't request any system reserved scopes + */ + private void ensureNoReservedScopes(ClientDetailsEntity client) { + // make sure a client doesn't get any special system scopes + Set requestedScope = scopeService.fromStrings(client.getScope()); + + requestedScope = scopeService.removeReservedScopes(requestedScope); + + client.setScope(scopeService.toStrings(requestedScope)); + } + + /** + * Load the sector identifier URI if it exists and check the redirect URIs against it + * @param client + */ + private void checkSectorIdentifierUri(ClientDetailsEntity client) { if (!Strings.isNullOrEmpty(client.getSectorIdentifierUri())) { try { List redirects = sectorRedirects.get(client.getSectorIdentifierUri()); @@ -132,20 +192,133 @@ public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) { } } - } catch (ExecutionException e) { - throw new IllegalArgumentException("Unable to load sector identifier URI: " + client.getSectorIdentifierUri()); + } catch (UncheckedExecutionException | ExecutionException e) { + throw new IllegalArgumentException("Unable to load sector identifier URI " + client.getSectorIdentifierUri() + ": " + e.getMessage()); } } + } + /** + * Make sure the client has the appropriate scope and grant type. + * @param client + */ + private void ensureRefreshTokenConsistency(ClientDetailsEntity client) { + if (client.getAuthorizedGrantTypes().contains("refresh_token") + || client.getScope().contains(SystemScopeService.OFFLINE_ACCESS)) { + client.getScope().add(SystemScopeService.OFFLINE_ACCESS); + client.getAuthorizedGrantTypes().add("refresh_token"); + } + } - // make sure a client doesn't get any special system scopes - client.setScope(scopeService.removeRestrictedScopes(client.getScope())); + /** + * If HEART mode is enabled, make sure the client meets the requirements: + * - Only one of authorization_code, implicit, or client_credentials can be used at a time + * - A redirect_uri must be registered with either authorization_code or implicit + * - A key must be registered + * - A client secret must not be generated + * - authorization_code and client_credentials must use the private_key authorization method + * @param client + */ + private void checkHeartMode(ClientDetailsEntity client) { + if (config.isHeartMode()) { + if (client.getGrantTypes().contains("authorization_code")) { + // make sure we don't have incompatible grant types + if (client.getGrantTypes().contains("implicit") || client.getGrantTypes().contains("client_credentials")) { + throw new IllegalArgumentException("[HEART mode] Incompatible grant types"); + } - ClientDetailsEntity c = clientRepository.saveClient(client); + // make sure we've got the right authentication method + if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { + throw new IllegalArgumentException("[HEART mode] Authorization code clients must use the private_key authentication method"); + } - statsService.resetCache(); + // make sure we've got a redirect URI + if (client.getRedirectUris().isEmpty()) { + throw new IllegalArgumentException("[HEART mode] Authorization code clients must register at least one redirect URI"); + } + } - return c; + if (client.getGrantTypes().contains("implicit")) { + // make sure we don't have incompatible grant types + if (client.getGrantTypes().contains("authorization_code") || client.getGrantTypes().contains("client_credentials") || client.getGrantTypes().contains("refresh_token")) { + throw new IllegalArgumentException("[HEART mode] Incompatible grant types"); + } + + // make sure we've got the right authentication method + if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) { + throw new IllegalArgumentException("[HEART mode] Implicit clients must use the none authentication method"); + } + + // make sure we've got a redirect URI + if (client.getRedirectUris().isEmpty()) { + throw new IllegalArgumentException("[HEART mode] Implicit clients must register at least one redirect URI"); + } + } + + if (client.getGrantTypes().contains("client_credentials")) { + // make sure we don't have incompatible grant types + if (client.getGrantTypes().contains("authorization_code") || client.getGrantTypes().contains("implicit") || client.getGrantTypes().contains("refresh_token")) { + throw new IllegalArgumentException("[HEART mode] Incompatible grant types"); + } + + // make sure we've got the right authentication method + if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { + throw new IllegalArgumentException("[HEART mode] Client credentials clients must use the private_key authentication method"); + } + + // make sure we've got a redirect URI + if (!client.getRedirectUris().isEmpty()) { + throw new IllegalArgumentException("[HEART mode] Client credentials clients must not register a redirect URI"); + } + + } + + if (client.getGrantTypes().contains("password")) { + throw new IllegalArgumentException("[HEART mode] Password grant type is forbidden"); + } + + // make sure we don't have a client secret + if (!Strings.isNullOrEmpty(client.getClientSecret())) { + throw new IllegalArgumentException("[HEART mode] Client secrets are not allowed"); + } + + // make sure we've got a key registered + if (client.getJwks() == null && Strings.isNullOrEmpty(client.getJwksUri())) { + throw new IllegalArgumentException("[HEART mode] All clients must have a key registered"); + } + + // make sure our redirect URIs each fit one of the allowed categories + if (client.getRedirectUris() != null && !client.getRedirectUris().isEmpty()) { + boolean localhost = false; + boolean remoteHttps = false; + boolean customScheme = false; + for (String uri : client.getRedirectUris()) { + UriComponents components = UriComponentsBuilder.fromUriString(uri).build(); + if (components.getScheme() == null) { + // this is a very unknown redirect URI + customScheme = true; + } else if (components.getScheme().equals("http")) { + // http scheme, check for localhost + if (components.getHost().equals("localhost") || components.getHost().equals("127.0.0.1")) { + localhost = true; + } else { + throw new IllegalArgumentException("[HEART mode] Can't have an http redirect URI on non-local host"); + } + } else if (components.getScheme().equals("https")) { + remoteHttps = true; + } else { + customScheme = true; + } + } + + // now we make sure the client has a URI in only one of each of the three categories + if (!((localhost ^ remoteHttps ^ customScheme) + && !(localhost && remoteHttps && customScheme))) { + throw new IllegalArgumentException("[HEART mode] Can't have more than one class of redirect URI"); + } + } + + } } /** @@ -198,6 +371,12 @@ public void deleteClient(ClientDetailsEntity client) throws InvalidClientExcepti whitelistedSiteService.remove(whitelistedSite); } + // clear out resource sets registered for this client + Collection resourceSets = resourceSetService.getAllForClient(client); + for (ResourceSet rs : resourceSets) { + resourceSetService.remove(rs); + } + // take care of the client itself clientRepository.deleteClient(client); @@ -208,16 +387,16 @@ public void deleteClient(ClientDetailsEntity client) throws InvalidClientExcepti /** * Update the oldClient with information from the newClient. The * id from oldClient is retained. - * + * * Checks to make sure the refresh grant type and * the scopes are set appropriately. - * + * * Checks to make sure the redirect URIs aren't blacklisted. - * + * * Attempts to load the redirect URI (possibly cached) to check the * sector identifier against the contents there. - * - * + * + * */ @Override public ClientDetailsEntity updateClient(ClientDetailsEntity oldClient, ClientDetailsEntity newClient) throws IllegalArgumentException { @@ -230,33 +409,19 @@ public ClientDetailsEntity updateClient(ClientDetailsEntity oldClient, ClientDet } // if the client is flagged to allow for refresh tokens, make sure it's got the right scope - if (newClient.isAllowRefresh()) { - newClient.getScope().add(SystemScopeService.OFFLINE_ACCESS); - } else { - newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS); - } + ensureRefreshTokenConsistency(newClient); + + // make sure we don't have both a JWKS and a JWKS URI + ensureKeyConsistency(newClient); + + // check consistency when using HEART mode + checkHeartMode(newClient); // check the sector URI - if (!Strings.isNullOrEmpty(newClient.getSectorIdentifierUri())) { - try { - List redirects = sectorRedirects.get(newClient.getSectorIdentifierUri()); - - if (newClient.getRegisteredRedirectUri() != null) { - for (String uri : newClient.getRegisteredRedirectUri()) { - if (!redirects.contains(uri)) { - throw new IllegalArgumentException("Requested Redirect URI " + uri + " is not listed at sector identifier " + redirects); - } - } - } - } catch (UncheckedExecutionException ue) { - throw new IllegalArgumentException("Unable to load sector identifier URI: " + newClient.getSectorIdentifierUri()); - } catch (ExecutionException e) { - throw new IllegalArgumentException("Unable to load sector identifier URI: " + newClient.getSectorIdentifierUri()); - } - } + checkSectorIdentifierUri(newClient); // make sure a client doesn't get any special system scopes - newClient.setScope(scopeService.removeRestrictedScopes(newClient.getScope())); + ensureNoReservedScopes(newClient); return clientRepository.updateClient(oldClient.getId(), newClient); } @@ -285,27 +450,38 @@ public ClientDetailsEntity generateClientId(ClientDetailsEntity client) { */ @Override public ClientDetailsEntity generateClientSecret(ClientDetailsEntity client) { - client.setClientSecret(Base64.encodeBase64URLSafeString(new BigInteger(512, new SecureRandom()).toByteArray()).replace("=", "")); + if (config.isHeartMode()) { + logger.error("[HEART mode] Can't generate a client secret, skipping step; client won't be saved due to invalid configuration"); + client.setClientSecret(null); + } else { + client.setClientSecret(Base64.encodeBase64URLSafeString(new BigInteger(512, new SecureRandom()).toByteArray()).replace("=", "")); + } return client; } /** * Utility class to load a sector identifier's set of authorized redirect URIs. - * + * * @author jricher * */ private class SectorIdentifierLoader extends CacheLoader> { - private HttpClient httpClient = new DefaultHttpClient(); - private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); - private RestTemplate restTemplate = new RestTemplate(httpFactory); + private HttpComponentsClientHttpRequestFactory httpFactory; + private RestTemplate restTemplate; private JsonParser parser = new JsonParser(); + SectorIdentifierLoader(HttpClient httpClient) { + this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + this.restTemplate = new RestTemplate(httpFactory); + } + @Override public List load(String key) throws Exception { if (!key.startsWith("https")) { - // TODO: this should optionally throw an error (#506) + if (config.isForceHttps()) { + throw new IllegalArgumentException("Sector identifier must start with https: " + key); + } logger.error("Sector identifier doesn't start with https, loading anyway..."); } @@ -314,7 +490,7 @@ public List load(String key) throws Exception { JsonElement json = parser.parse(jsonString); if (json.isJsonArray()) { - List redirectUris = new ArrayList(); + List redirectUris = new ArrayList<>(); for (JsonElement el : json.getAsJsonArray()) { redirectUris.add(el.getAsString()); } @@ -323,7 +499,7 @@ public List load(String key) throws Exception { return redirectUris; } else { - return null; + throw new IllegalArgumentException("JSON Format Error"); } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java index 8030105af7..641bf96faa 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java @@ -1,24 +1,32 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; +import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE_METHOD; +import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_VERIFIER; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Date; import java.util.HashSet; @@ -26,10 +34,14 @@ import java.util.Set; import java.util.UUID; +import org.mitre.data.AbstractPageOperationTemplate; +import org.mitre.data.DefaultPageCriteria; import org.mitre.oauth2.model.AuthenticationHolderEntity; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.PKCEAlgorithm; +import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.repository.AuthenticationHolderRepository; import org.mitre.oauth2.repository.OAuth2TokenRepository; import org.mitre.oauth2.service.ClientDetailsEntityService; @@ -43,6 +55,7 @@ import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.provider.OAuth2Authentication; @@ -50,20 +63,25 @@ import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import com.google.common.collect.Sets; +import com.google.common.base.Strings; +import com.nimbusds.jose.util.Base64URL; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.PlainJWT; /** * @author jricher - * + * */ @Service("defaultOAuth2ProviderTokenService") public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityService { - private static Logger logger = LoggerFactory.getLogger(DefaultOAuth2ProviderTokenService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultOAuth2ProviderTokenService.class); @Autowired private OAuth2TokenRepository tokenRepository; @@ -80,60 +98,103 @@ public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityServi @Autowired private SystemScopeService scopeService; - @Override - public Set getAllAccessTokensForUser(String id) { - - Set all = tokenRepository.getAllAccessTokens(); - Set results = Sets.newLinkedHashSet(); - - for (OAuth2AccessTokenEntity token : all) { - if (token.getAuthenticationHolder().getAuthentication().getName().equals(id)) { - results.add(token); - } - } + @Autowired + private ApprovedSiteService approvedSiteService; - return results; + @Override + public Set getAllAccessTokensForUser(String userName) { + return tokenRepository.getAccessTokensByUserName(userName); } - @Override - public Set getAllRefreshTokensForUser(String id) { - Set all = tokenRepository.getAllRefreshTokens(); - Set results = Sets.newLinkedHashSet(); - - for (OAuth2RefreshTokenEntity token : all) { - if (token.getAuthenticationHolder().getAuthentication().getName().equals(id)) { - results.add(token); - } - } - - return results; + public Set getAllRefreshTokensForUser(String userName) { + return tokenRepository.getRefreshTokensByUserName(userName); } @Override public OAuth2AccessTokenEntity getAccessTokenById(Long id) { - return tokenRepository.getAccessTokenById(id); + return clearExpiredAccessToken(tokenRepository.getAccessTokenById(id)); } @Override public OAuth2RefreshTokenEntity getRefreshTokenById(Long id) { - return tokenRepository.getRefreshTokenById(id); + return clearExpiredRefreshToken(tokenRepository.getRefreshTokenById(id)); } - @Autowired - private ApprovedSiteService approvedSiteService; + /** + * Utility function to delete an access token that's expired before returning it. + * @param token the token to check + * @return null if the token is null or expired, the input token (unchanged) if it hasn't + */ + private OAuth2AccessTokenEntity clearExpiredAccessToken(OAuth2AccessTokenEntity token) { + if (token == null) { + return null; + } else if (token.isExpired()) { + // immediately revoke expired token + logger.debug("Clearing expired access token: " + token.getValue()); + revokeAccessToken(token); + return null; + } else { + return token; + } + } + /** + * Utility function to delete a refresh token that's expired before returning it. + * @param token the token to check + * @return null if the token is null or expired, the input token (unchanged) if it hasn't + */ + private OAuth2RefreshTokenEntity clearExpiredRefreshToken(OAuth2RefreshTokenEntity token) { + if (token == null) { + return null; + } else if (token.isExpired()) { + // immediately revoke expired token + logger.debug("Clearing expired refresh token: " + token.getValue()); + revokeRefreshToken(token); + return null; + } else { + return token; + } + } @Override + @Transactional(value="defaultTransactionManager") public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentication) throws AuthenticationException, InvalidClientException { if (authentication != null && authentication.getOAuth2Request() != null) { // look up our client - OAuth2Request clientAuth = authentication.getOAuth2Request(); + OAuth2Request request = authentication.getOAuth2Request(); - ClientDetailsEntity client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + ClientDetailsEntity client = clientDetailsService.loadClientByClientId(request.getClientId()); if (client == null) { - throw new InvalidClientException("Client not found: " + clientAuth.getClientId()); + throw new InvalidClientException("Client not found: " + request.getClientId()); + } + + // handle the PKCE code challenge if present + if (request.getExtensions().containsKey(CODE_CHALLENGE)) { + String challenge = (String) request.getExtensions().get(CODE_CHALLENGE); + PKCEAlgorithm alg = PKCEAlgorithm.parse((String) request.getExtensions().get(CODE_CHALLENGE_METHOD)); + + String verifier = request.getRequestParameters().get(CODE_VERIFIER); + + if (alg.equals(PKCEAlgorithm.plain)) { + // do a direct string comparison + if (!challenge.equals(verifier)) { + throw new InvalidRequestException("Code challenge and verifier do not match"); + } + } else if (alg.equals(PKCEAlgorithm.S256)) { + // hash the verifier + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + String hash = Base64URL.encode(digest.digest(verifier.getBytes(StandardCharsets.US_ASCII))).toString(); + if (!challenge.equals(hash)) { + throw new InvalidRequestException("Code challenge and verifier do not match"); + } + } catch (NoSuchAlgorithmException e) { + logger.error("Unknown algorithm for PKCE digest", e); + } + } + } OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity();//accessTokenFactory.createNewAccessToken(); @@ -144,10 +205,12 @@ public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentica // inherit the scope from the auth, but make a new set so it is //not unmodifiable. Unmodifiables don't play nicely with Eclipselink, which //wants to use the clone operation. - Set scopes = Sets.newHashSet(clientAuth.getScope()); + Set scopes = scopeService.fromStrings(request.getScope()); + // remove any of the special system scopes - scopes = scopeService.removeRestrictedScopes(scopes); - token.setScope(scopes); + scopes = scopeService.removeReservedScopes(scopes); + + token.setScope(scopeService.toStrings(scopes)); // make it expire if necessary if (client.getAccessTokenValiditySeconds() != null && client.getAccessTokenValiditySeconds() > 0) { @@ -163,56 +226,27 @@ public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentica token.setAuthenticationHolder(authHolder); // attach a refresh token, if this client is allowed to request them and the user gets the offline scope - if (client.isAllowRefresh() && scopes.contains(SystemScopeService.OFFLINE_ACCESS)) { - OAuth2RefreshTokenEntity refreshToken = new OAuth2RefreshTokenEntity(); //refreshTokenFactory.createNewRefreshToken(); - JWTClaimsSet refreshClaims = new JWTClaimsSet(); - - - // make it expire if necessary - if (client.getRefreshTokenValiditySeconds() != null) { - Date expiration = new Date(System.currentTimeMillis() + (client.getRefreshTokenValiditySeconds() * 1000L)); - refreshToken.setExpiration(expiration); - refreshClaims.setExpirationTime(expiration); - } - - // set a random identifier - refreshClaims.setJWTID(UUID.randomUUID().toString()); - - // TODO: add issuer fields, signature to JWT - - PlainJWT refreshJwt = new PlainJWT(refreshClaims); - refreshToken.setJwt(refreshJwt); - - //Add the authentication - refreshToken.setAuthenticationHolder(authHolder); - refreshToken.setClient(client); - - - - // save the token first so that we can set it to a member of the access token (NOTE: is this step necessary?) - OAuth2RefreshTokenEntity savedRefreshToken = tokenRepository.saveRefreshToken(refreshToken); + if (client.isAllowRefresh() && token.getScope().contains(SystemScopeService.OFFLINE_ACCESS)) { + OAuth2RefreshTokenEntity savedRefreshToken = createRefreshToken(client, authHolder); token.setRefreshToken(savedRefreshToken); } - - OAuth2AccessTokenEntity enhancedToken = (OAuth2AccessTokenEntity) tokenEnhancer.enhance(token, authentication); - - OAuth2AccessTokenEntity savedToken = tokenRepository.saveAccessToken(enhancedToken); //Add approved site reference, if any OAuth2Request originalAuthRequest = authHolder.getAuthentication().getOAuth2Request(); if (originalAuthRequest.getExtensions() != null && originalAuthRequest.getExtensions().containsKey("approved_site")) { - Long apId = (Long) originalAuthRequest.getExtensions().get("approved_site"); + Long apId = Long.parseLong((String) originalAuthRequest.getExtensions().get("approved_site")); ApprovedSite ap = approvedSiteService.getById(apId); - Set apTokens = ap.getApprovedAccessTokens(); - apTokens.add(savedToken); - ap.setApprovedAccessTokens(apTokens); - approvedSiteService.save(ap); + token.setApprovedSite(ap); } + OAuth2AccessTokenEntity enhancedToken = (OAuth2AccessTokenEntity) tokenEnhancer.enhance(token, authentication); + + OAuth2AccessTokenEntity savedToken = saveAccessToken(enhancedToken); + if (savedToken.getRefreshToken() != null) { tokenRepository.saveRefreshToken(savedToken.getRefreshToken()); // make sure we save any changes that might have been enhanced } @@ -223,12 +257,49 @@ public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentica throw new AuthenticationCredentialsNotFoundException("No authentication credentials found"); } + + private OAuth2RefreshTokenEntity createRefreshToken(ClientDetailsEntity client, AuthenticationHolderEntity authHolder) { + OAuth2RefreshTokenEntity refreshToken = new OAuth2RefreshTokenEntity(); //refreshTokenFactory.createNewRefreshToken(); + JWTClaimsSet.Builder refreshClaims = new JWTClaimsSet.Builder(); + + + // make it expire if necessary + if (client.getRefreshTokenValiditySeconds() != null) { + Date expiration = new Date(System.currentTimeMillis() + (client.getRefreshTokenValiditySeconds() * 1000L)); + refreshToken.setExpiration(expiration); + refreshClaims.expirationTime(expiration); + } + + // set a random identifier + refreshClaims.jwtID(UUID.randomUUID().toString()); + + // TODO: add issuer fields, signature to JWT + + PlainJWT refreshJwt = new PlainJWT(refreshClaims.build()); + refreshToken.setJwt(refreshJwt); + + //Add the authentication + refreshToken.setAuthenticationHolder(authHolder); + refreshToken.setClient(client); + + // save the token first so that we can set it to a member of the access token (NOTE: is this step necessary?) + OAuth2RefreshTokenEntity savedRefreshToken = tokenRepository.saveRefreshToken(refreshToken); + return savedRefreshToken; + } + @Override + @Transactional(value="defaultTransactionManager") public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, TokenRequest authRequest) throws AuthenticationException { + + if (Strings.isNullOrEmpty(refreshTokenValue)) { + // throw an invalid token exception if there's no refresh token value at all + throw new InvalidTokenException("Invalid refresh token: " + refreshTokenValue); + } - OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenByValue(refreshTokenValue); + OAuth2RefreshTokenEntity refreshToken = clearExpiredRefreshToken(tokenRepository.getRefreshTokenByValue(refreshTokenValue)); if (refreshToken == null) { + // throw an invalid token exception if we couldn't find the token throw new InvalidTokenException("Invalid refresh token: " + refreshTokenValue); } @@ -236,39 +307,47 @@ public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, Toke AuthenticationHolderEntity authHolder = refreshToken.getAuthenticationHolder(); + // make sure that the client requesting the token is the one who owns the refresh token + ClientDetailsEntity requestingClient = clientDetailsService.loadClientByClientId(authRequest.getClientId()); + if (!client.getClientId().equals(requestingClient.getClientId())) { + tokenRepository.removeRefreshToken(refreshToken); + throw new InvalidClientException("Client does not own the presented refresh token"); + } + //Make sure this client allows access token refreshing if (!client.isAllowRefresh()) { throw new InvalidClientException("Client does not allow refreshing access token!"); } // clear out any access tokens - // TODO: make this a configurable option - tokenRepository.clearAccessTokensForRefreshToken(refreshToken); + if (client.isClearAccessTokensOnRefresh()) { + tokenRepository.clearAccessTokensForRefreshToken(refreshToken); + } if (refreshToken.isExpired()) { tokenRepository.removeRefreshToken(refreshToken); throw new InvalidTokenException("Expired refresh token: " + refreshTokenValue); } - // TODO: have the option to recycle the refresh token here, too - // for now, we just reuse it as long as it's valid, which is the original intent - OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); // get the stored scopes from the authentication holder's authorization request; these are the scopes associated with the refresh token - Set refreshScopes = new HashSet(refreshToken.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope()); + Set refreshScopesRequested = new HashSet<>(refreshToken.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope()); + Set refreshScopes = scopeService.fromStrings(refreshScopesRequested); // remove any of the special system scopes - refreshScopes = scopeService.removeRestrictedScopes(refreshScopes); + refreshScopes = scopeService.removeReservedScopes(refreshScopes); + + Set scopeRequested = authRequest.getScope() == null ? new HashSet() : new HashSet<>(authRequest.getScope()); + Set scope = scopeService.fromStrings(scopeRequested); - Set scope = authRequest.getScope() == null ? new HashSet() : new HashSet(authRequest.getScope()); // remove any of the special system scopes - scope = scopeService.removeRestrictedScopes(scope); + scope = scopeService.removeReservedScopes(scope); if (scope != null && !scope.isEmpty()) { // ensure a proper subset of scopes if (refreshScopes != null && refreshScopes.containsAll(scope)) { // set the scope of the new access token if requested - token.setScope(scope); + token.setScope(scopeService.toStrings(scope)); } else { String errorMsg = "Up-scoping is not allowed."; logger.error(errorMsg); @@ -276,7 +355,7 @@ public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, Toke } } else { // otherwise inherit the scope of the refresh token (if it's there -- this can return a null scope set) - token.setScope(refreshScopes); + token.setScope(scopeService.toStrings(refreshScopes)); } token.setClient(client); @@ -286,7 +365,17 @@ public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, Toke token.setExpiration(expiration); } - token.setRefreshToken(refreshToken); + if (client.isReuseRefreshToken()) { + // if the client re-uses refresh tokens, do that + token.setRefreshToken(refreshToken); + } else { + // otherwise, make a new refresh token + OAuth2RefreshTokenEntity newRefresh = createRefreshToken(client, authHolder); + token.setRefreshToken(newRefresh); + + // clean up the old refresh token + tokenRepository.removeRefreshToken(refreshToken); + } token.setAuthenticationHolder(authHolder); @@ -295,25 +384,17 @@ public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, Toke tokenRepository.saveAccessToken(token); return token; - } @Override public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException { - - OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenByValue(accessTokenValue); + OAuth2AccessTokenEntity accessToken = clearExpiredAccessToken(tokenRepository.getAccessTokenByValue(accessTokenValue)); if (accessToken == null) { throw new InvalidTokenException("Invalid access token: " + accessTokenValue); + } else { + return accessToken.getAuthenticationHolder().getAuthentication(); } - - if (accessToken.isExpired()) { - //tokenRepository.removeAccessToken(accessToken); - revokeAccessToken(accessToken); - throw new InvalidTokenException("Expired access token: " + accessTokenValue); - } - - return accessToken.getAuthenticationHolder().getAuthentication(); } @@ -322,11 +403,10 @@ public OAuth2Authentication loadAuthentication(String accessTokenValue) throws A */ @Override public OAuth2AccessTokenEntity readAccessToken(String accessTokenValue) throws AuthenticationException { - OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenByValue(accessTokenValue); + OAuth2AccessTokenEntity accessToken = clearExpiredAccessToken(tokenRepository.getAccessTokenByValue(accessTokenValue)); if (accessToken == null) { throw new InvalidTokenException("Access token for value " + accessTokenValue + " was not found"); - } - else { + } else { return accessToken; } } @@ -336,10 +416,8 @@ public OAuth2AccessTokenEntity readAccessToken(String accessTokenValue) throws A */ @Override public OAuth2AccessTokenEntity getAccessToken(OAuth2Authentication authentication) { - - OAuth2AccessTokenEntity accessToken = tokenRepository.getByAuthentication(authentication); - - return accessToken; + // TODO: implement this against the new service (#825) + throw new UnsupportedOperationException("Unable to look up access token from authentication object."); } /** @@ -360,6 +438,7 @@ public OAuth2RefreshTokenEntity getRefreshToken(String refreshTokenValue) throws * Revoke a refresh token and all access tokens issued to it. */ @Override + @Transactional(value="defaultTransactionManager") public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { tokenRepository.clearAccessTokensForRefreshToken(refreshToken); tokenRepository.removeRefreshToken(refreshToken); @@ -369,22 +448,16 @@ public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { * Revoke an access token. */ @Override + @Transactional(value="defaultTransactionManager") public void revokeAccessToken(OAuth2AccessTokenEntity accessToken) { tokenRepository.removeAccessToken(accessToken); } - - /* (non-Javadoc) - * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getAccessTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) - */ @Override public List getAccessTokensForClient(ClientDetailsEntity client) { return tokenRepository.getAccessTokensForClient(client); } - /* (non-Javadoc) - * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getRefreshTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) - */ @Override public List getRefreshTokensForClient(ClientDetailsEntity client) { return tokenRepository.getRefreshTokensForClient(client); @@ -395,57 +468,66 @@ public List getRefreshTokensForClient(ClientDetailsEnt */ @Override public void clearExpiredTokens() { - logger.info("Cleaning out all expired tokens"); - - Collection accessTokens = getExpiredAccessTokens(); - logger.info("Found " + accessTokens.size() + " expired access tokens"); - for (OAuth2AccessTokenEntity oAuth2AccessTokenEntity : accessTokens) { - try { - revokeAccessToken(oAuth2AccessTokenEntity); - } catch (IllegalArgumentException e) { - //An ID token is deleted with its corresponding access token, but then the ID token is on the list of expired tokens as well and there is - //nothing in place to distinguish it from any other. - //An attempt to delete an already deleted token returns an error, stopping the cleanup dead. We need it to keep going. + logger.debug("Cleaning out all expired tokens"); + + new AbstractPageOperationTemplate("clearExpiredAccessTokens") { + @Override + public Collection fetchPage() { + return tokenRepository.getAllExpiredAccessTokens(new DefaultPageCriteria()); } - } - Collection refreshTokens = getExpiredRefreshTokens(); - logger.info("Found " + refreshTokens.size() + " expired refresh tokens"); - for (OAuth2RefreshTokenEntity oAuth2RefreshTokenEntity : refreshTokens) { - revokeRefreshToken(oAuth2RefreshTokenEntity); - } + @Override + public void doOperation(OAuth2AccessTokenEntity item) { + revokeAccessToken(item); + } + }.execute(); - Collection authHolders = getOrphanedAuthenticationHolders(); - logger.info("Found " + authHolders.size() + " orphaned authentication holders"); - for(AuthenticationHolderEntity authHolder : authHolders) { - authenticationHolderRepository.remove(authHolder); - } - } + new AbstractPageOperationTemplate("clearExpiredRefreshTokens") { + @Override + public Collection fetchPage() { + return tokenRepository.getAllExpiredRefreshTokens(new DefaultPageCriteria()); + } - private Collection getExpiredAccessTokens() { - return Sets.newHashSet(tokenRepository.getAllExpiredAccessTokens()); - } + @Override + public void doOperation(OAuth2RefreshTokenEntity item) { + revokeRefreshToken(item); + } + }.execute(); - private Collection getExpiredRefreshTokens() { - return Sets.newHashSet(tokenRepository.getAllExpiredRefreshTokens()); - } + new AbstractPageOperationTemplate("clearExpiredAuthenticationHolders") { + @Override + public Collection fetchPage() { + return authenticationHolderRepository.getOrphanedAuthenticationHolders(new DefaultPageCriteria()); + } - private Collection getOrphanedAuthenticationHolders() { - return Sets.newHashSet(authenticationHolderRepository.getOrphanedAuthenticationHolders()); + @Override + public void doOperation(AuthenticationHolderEntity item) { + authenticationHolderRepository.remove(item); + } + }.execute(); } /* (non-Javadoc) * @see org.mitre.oauth2.service.OAuth2TokenEntityService#saveAccessToken(org.mitre.oauth2.model.OAuth2AccessTokenEntity) */ @Override + @Transactional(value="defaultTransactionManager") public OAuth2AccessTokenEntity saveAccessToken(OAuth2AccessTokenEntity accessToken) { - return tokenRepository.saveAccessToken(accessToken); + OAuth2AccessTokenEntity newToken = tokenRepository.saveAccessToken(accessToken); + + // if the old token has any additional information for the return from the token endpoint, carry it through here after save + if (accessToken.getAdditionalInformation() != null && !accessToken.getAdditionalInformation().isEmpty()) { + newToken.getAdditionalInformation().putAll(accessToken.getAdditionalInformation()); + } + + return newToken; } /* (non-Javadoc) * @see org.mitre.oauth2.service.OAuth2TokenEntityService#saveRefreshToken(org.mitre.oauth2.model.OAuth2RefreshTokenEntity) */ @Override + @Transactional(value="defaultTransactionManager") public OAuth2RefreshTokenEntity saveRefreshToken(OAuth2RefreshTokenEntity refreshToken) { return tokenRepository.saveRefreshToken(refreshToken); } @@ -464,12 +546,18 @@ public void setTokenEnhancer(TokenEnhancer tokenEnhancer) { this.tokenEnhancer = tokenEnhancer; } - /* (non-Javadoc) - * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getAccessTokenForIdToken(org.mitre.oauth2.model.OAuth2AccessTokenEntity) - */ @Override - public OAuth2AccessTokenEntity getAccessTokenForIdToken(OAuth2AccessTokenEntity idToken) { - return tokenRepository.getAccessTokenForIdToken(idToken); - } + public OAuth2AccessTokenEntity getRegistrationAccessTokenForClient(ClientDetailsEntity client) { + List allTokens = getAccessTokensForClient(client); + + for (OAuth2AccessTokenEntity token : allTokens) { + if ((token.getScope().contains(SystemScopeService.REGISTRATION_TOKEN_SCOPE) || token.getScope().contains(SystemScopeService.RESOURCE_TOKEN_SCOPE)) + && token.getScope().size() == 1) { + // if it only has the registration scope, then it's a registration token + return token; + } + } + return null; + } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultSystemScopeService.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultSystemScopeService.java index 167c447c82..21474fe6e0 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultSystemScopeService.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultSystemScopeService.java @@ -1,26 +1,26 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; import java.util.LinkedHashSet; -import java.util.List; import java.util.Set; import org.mitre.oauth2.model.SystemScope; @@ -30,13 +30,10 @@ import org.springframework.stereotype.Service; import com.google.common.base.Function; -import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Predicates; -import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; import com.google.common.collect.Sets; /** @@ -56,21 +53,17 @@ public boolean apply(SystemScope input) { } }; - - private Predicate isDynReg = new Predicate() { + private Predicate isRestricted = new Predicate() { @Override public boolean apply(SystemScope input) { - return (input != null && input.isAllowDynReg()); + return (input != null && input.isRestricted()); } }; - private Predicate isRestricted = new Predicate() { + private Predicate isReserved = new Predicate() { @Override - public boolean apply(String input) { - return (input != null && - !input.equals(ID_TOKEN_SCOPE) && - !input.equals(REGISTRATION_TOKEN_SCOPE) && - !input.equals(RESOURCE_TOKEN_SCOPE)); + public boolean apply(SystemScope input) { + return (input != null && getReserved().contains(input)); } }; @@ -80,20 +73,11 @@ public SystemScope apply(String input) { if (Strings.isNullOrEmpty(input)) { return null; } else { - List parts = parseStructuredScopeValue(input); - String base = parts.get(0); // the first part is the base // get the real scope if it's available - SystemScope s = getByValue(base); + SystemScope s = getByValue(input); if (s == null) { // make a fake one otherwise - s = new SystemScope(base); - if (parts.size() > 1) { - s.setStructured(true); - } - } - - if (s.isStructured() && parts.size() > 1) { - s.setStructuredValue(parts.get(1)); + s = new SystemScope(input); } return s; @@ -107,11 +91,7 @@ public String apply(SystemScope input) { if (input == null) { return null; } else { - if (input.isStructured() && !Strings.isNullOrEmpty(input.getStructuredValue())) { - return Joiner.on(":").join(input.getValue(), input.getStructuredValue()); - } else { - return input.getValue(); - } + return input.getValue(); } } }; @@ -124,22 +104,6 @@ public Set getAll() { return repository.getAll(); } - /* (non-Javadoc) - * @see org.mitre.oauth2.service.SystemScopeService#getDefaults() - */ - @Override - public Set getDefaults() { - return Sets.filter(getAll(), isDefault); - } - - /* (non-Javadoc) - * @see org.mitre.oauth2.service.SystemScopeService#getDynReg() - */ - @Override - public Set getDynReg() { - return Sets.filter(getAll(), isDynReg); - } - /* (non-Javadoc) * @see org.mitre.oauth2.service.SystemScopeService#getById(java.lang.Long) */ @@ -170,7 +134,11 @@ public void remove(SystemScope scope) { */ @Override public SystemScope save(SystemScope scope) { - return repository.save(scope); + if (!isReserved.apply(scope)) { // don't allow saving of reserved scopes + return repository.save(scope); + } else { + return null; + } } /* (non-Javadoc) @@ -181,7 +149,7 @@ public Set fromStrings(Set scope) { if (scope == null) { return null; } else { - return new LinkedHashSet(Collections2.filter(Collections2.transform(scope, stringToSystemScope), Predicates.notNull())); + return new LinkedHashSet<>(Collections2.filter(Collections2.transform(scope, stringToSystemScope), Predicates.notNull())); } } @@ -193,15 +161,10 @@ public Set toStrings(Set scope) { if (scope == null) { return null; } else { - return new LinkedHashSet(Collections2.filter(Collections2.transform(scope, systemScopeToString), Predicates.notNull())); + return new LinkedHashSet<>(Collections2.filter(Collections2.transform(scope, systemScopeToString), Predicates.notNull())); } } - // parse a structured scope string into its components - private List parseStructuredScopeValue(String value) { - return Lists.newArrayList(Splitter.on(":").split(value)); - } - /* (non-Javadoc) * @see org.mitre.oauth2.service.SystemScopeService#scopesMatch(java.util.Set, java.util.Set) */ @@ -214,22 +177,7 @@ public boolean scopesMatch(Set expected, Set actual) { for (SystemScope actScope : act) { // first check to see if there's an exact match if (!ex.contains(actScope)) { - // we didn't find an exact match - if (actScope.isStructured() && !Strings.isNullOrEmpty(actScope.getStructuredValue())) { - // if we didn't get an exact match but the actual scope is structured, we need to check further - - // first, find the "base" scope for this - SystemScope base = getByValue(actScope.getValue()); - if (!ex.contains(base)) { - // if the expected doesn't contain the base scope, fail - return false; - } else { - // we did find an exact match, need to check the rest - } - } else { - // the scope wasn't structured, fail now - return false; - } + return false; } else { // if we did find an exact match, we need to check the rest } @@ -241,10 +189,34 @@ public boolean scopesMatch(Set expected, Set actual) { } @Override - public Set removeRestrictedScopes(Set scopes) { - return new LinkedHashSet(Collections2.filter(scopes, isRestricted)); + public Set getDefaults() { + return Sets.filter(getAll(), isDefault); + } + + + @Override + public Set getReserved() { + return reservedScopes; + } + + @Override + public Set getRestricted() { + return Sets.filter(getAll(), isRestricted); } + @Override + public Set getUnrestricted() { + return Sets.filter(getAll(), Predicates.not(isRestricted)); + } + @Override + public Set removeRestrictedAndReservedScopes(Set scopes) { + return Sets.filter(scopes, Predicates.not(Predicates.or(isRestricted, isReserved))); + } + + @Override + public Set removeReservedScopes(Set scopes) { + return Sets.filter(scopes, Predicates.not(isReserved)); + } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/token/ChainedTokenGranter.java b/openid-connect-server/src/main/java/org/mitre/oauth2/token/ChainedTokenGranter.java index 4213df3b4c..c53596f26b 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/token/ChainedTokenGranter.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/token/ChainedTokenGranter.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.token; @@ -45,7 +46,7 @@ @Component("chainedTokenGranter") public class ChainedTokenGranter extends AbstractTokenGranter { - private static final String grantType = "urn:ietf:params:oauth:grant_type:redelegate"; + public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant_type:redelegate"; // keep down-cast versions so we can get to the right queries private OAuth2TokenEntityService tokenServices; @@ -53,11 +54,11 @@ public class ChainedTokenGranter extends AbstractTokenGranter { /** * @param tokenServices * @param clientDetailsService - * @param grantType + * @param GRANT_TYPE */ @Autowired public ChainedTokenGranter(OAuth2TokenEntityService tokenServices, ClientDetailsEntityService clientDetailsService, OAuth2RequestFactory requestFactory) { - super(tokenServices, clientDetailsService, requestFactory, grantType); + super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE); this.tokenServices = tokenServices; } @@ -75,12 +76,12 @@ protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, Tok Set requestedScopes = tokenRequest.getScope(); if (requestedScopes == null) { - requestedScopes = new HashSet(); + requestedScopes = new HashSet<>(); } // do a check on the requested scopes -- if they exactly match the client scopes, they were probably shadowed by the token granter if (client.getScope().equals(requestedScopes)) { - requestedScopes = new HashSet(); + requestedScopes = new HashSet<>(); } // if our scopes are a valid subset of what's allowed, we can continue diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/token/DeviceTokenGranter.java b/openid-connect-server/src/main/java/org/mitre/oauth2/token/DeviceTokenGranter.java new file mode 100644 index 0000000000..f7e185e530 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/token/DeviceTokenGranter.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.token; + +import java.util.Date; + +import org.mitre.oauth2.exception.AuthorizationPendingException; +import org.mitre.oauth2.exception.DeviceCodeExpiredException; +import org.mitre.oauth2.model.DeviceCode; +import org.mitre.oauth2.service.DeviceCodeService; +import org.mitre.oauth2.web.DeviceEndpoint; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.TokenRequest; +import org.springframework.security.oauth2.provider.token.AbstractTokenGranter; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.stereotype.Component; + +/** + * Implements https://tools.ietf.org/html/draft-ietf-oauth-device-flow + * + * @see DeviceEndpoint + * + * @author jricher + * + */ +@Component("deviceTokenGranter") +public class DeviceTokenGranter extends AbstractTokenGranter { + + public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; + + @Autowired + private DeviceCodeService deviceCodeService; + + /** + * @param tokenServices + * @param clientDetailsService + * @param requestFactory + * @param grantType + */ + protected DeviceTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) { + super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#getOAuth2Authentication(org.springframework.security.oauth2.provider.ClientDetails, org.springframework.security.oauth2.provider.TokenRequest) + */ + @Override + protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { + + String deviceCode = tokenRequest.getRequestParameters().get("device_code"); + + // look up the device code and consume it + DeviceCode dc = deviceCodeService.findDeviceCode(deviceCode, client); + + if (dc != null) { + + // make sure the code hasn't expired yet + if (dc.getExpiration() != null && dc.getExpiration().before(new Date())) { + + deviceCodeService.clearDeviceCode(deviceCode, client); + + throw new DeviceCodeExpiredException("Device code has expired " + deviceCode); + + } else if (!dc.isApproved()) { + + // still waiting for approval + throw new AuthorizationPendingException("Authorization pending for code " + deviceCode); + + } else { + // inherit the (approved) scopes from the original request + tokenRequest.setScope(dc.getScope()); + + OAuth2Authentication auth = new OAuth2Authentication(getRequestFactory().createOAuth2Request(client, tokenRequest), dc.getAuthenticationHolder().getUserAuth()); + + deviceCodeService.clearDeviceCode(deviceCode, client); + + return auth; + } + } else { + throw new InvalidGrantException("Invalid device code: " + deviceCode); + } + + } + + + + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/token/JWTAssertionTokenGranter.java b/openid-connect-server/src/main/java/org/mitre/oauth2/token/JWTAssertionTokenGranter.java new file mode 100644 index 0000000000..02217fc48e --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/token/JWTAssertionTokenGranter.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.oauth2.token; + +import java.text.ParseException; + +import org.mitre.jwt.assertion.AssertionValidator; +import org.mitre.oauth2.assertion.AssertionOAuth2RequestFactory; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.mitre.openid.connect.assertion.JWTBearerAssertionAuthenticationToken; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.TokenRequest; +import org.springframework.security.oauth2.provider.token.AbstractTokenGranter; +import org.springframework.stereotype.Component; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + +/** + * @author jricher + * + */ +@Component("jwtAssertionTokenGranter") +public class JWTAssertionTokenGranter extends AbstractTokenGranter { + + private static final String grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + + @Autowired + @Qualifier("jwtAssertionValidator") + private AssertionValidator validator; + + @Autowired + private AssertionOAuth2RequestFactory assertionFactory; + + @Autowired + public JWTAssertionTokenGranter(OAuth2TokenEntityService tokenServices, ClientDetailsEntityService clientDetailsService, OAuth2RequestFactory requestFactory) { + super(tokenServices, clientDetailsService, requestFactory, grantType); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#getOAuth2Authentication(org.springframework.security.oauth2.provider.AuthorizationRequest) + */ + @Override + protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) throws AuthenticationException, InvalidTokenException { + // read and load up the existing token + try { + String incomingAssertionValue = tokenRequest.getRequestParameters().get("assertion"); + JWT assertion = JWTParser.parse(incomingAssertionValue); + + if (validator.isValid(assertion)) { + + // our validator says it's OK, time to make a token from it + // the real work happens in the assertion factory and the token services + return new OAuth2Authentication(assertionFactory.createOAuth2Request(client, tokenRequest, assertion), + new JWTBearerAssertionAuthenticationToken(assertion, client.getAuthorities())); + + } else { + logger.warn("Incoming assertion did not pass validator, rejecting"); + return null; + } + + } catch (ParseException e) { + logger.warn("Unable to parse incoming assertion"); + } + + // if we had made a token, we'd have returned it by now, so return null here to close out with no created token + return null; + + } + + + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/token/JwtAssertionTokenGranter.java b/openid-connect-server/src/main/java/org/mitre/oauth2/token/JwtAssertionTokenGranter.java deleted file mode 100644 index 79278222c6..0000000000 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/token/JwtAssertionTokenGranter.java +++ /dev/null @@ -1,204 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.oauth2.token; - -import java.text.ParseException; -import java.util.Date; - -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; -import org.mitre.oauth2.model.ClientDetailsEntity; -import org.mitre.oauth2.model.OAuth2AccessTokenEntity; -import org.mitre.oauth2.service.ClientDetailsEntityService; -import org.mitre.oauth2.service.OAuth2TokenEntityService; -import org.mitre.oauth2.service.SystemScopeService; -import org.mitre.openid.connect.config.ConfigurationPropertiesBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.exceptions.InvalidClientException; -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.OAuth2RequestFactory; -import org.springframework.security.oauth2.provider.TokenRequest; -import org.springframework.security.oauth2.provider.token.AbstractTokenGranter; -import org.springframework.stereotype.Component; - -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.JWTParser; -import com.nimbusds.jwt.SignedJWT; - -/** - * @author jricher - * - */ -@Component("jwtAssertionTokenGranter") -public class JwtAssertionTokenGranter extends AbstractTokenGranter { - - private static final String grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"; - - // keep down-cast versions so we can get to the right queries - private OAuth2TokenEntityService tokenServices; - - @Autowired - private JwtSigningAndValidationService jwtService; - - @Autowired - private ConfigurationPropertiesBean config; - - @Autowired - public JwtAssertionTokenGranter(OAuth2TokenEntityService tokenServices, ClientDetailsEntityService clientDetailsService, OAuth2RequestFactory requestFactory) { - super(tokenServices, clientDetailsService, requestFactory, grantType); - this.tokenServices = tokenServices; - } - - /* (non-Javadoc) - * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#getOAuth2Authentication(org.springframework.security.oauth2.provider.AuthorizationRequest) - */ - @Override - protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) throws AuthenticationException, InvalidTokenException { - // read and load up the existing token - String incomingTokenValue = tokenRequest.getRequestParameters().get("assertion"); - OAuth2AccessTokenEntity incomingToken = tokenServices.readAccessToken(incomingTokenValue); - - if (incomingToken.getScope().contains(SystemScopeService.ID_TOKEN_SCOPE)) { - - if (!client.getClientId().equals(tokenRequest.getClientId())) { - throw new InvalidClientException("Not the right client for this token"); - } - - // it's an ID token, process it accordingly - - try { - - // TODO: make this use a more specific idtoken class - JWT idToken = JWTParser.parse(incomingTokenValue); - - OAuth2AccessTokenEntity accessToken = tokenServices.getAccessTokenForIdToken(incomingToken); - - if (accessToken != null) { - - //OAuth2AccessTokenEntity newIdToken = tokenServices.get - - OAuth2AccessTokenEntity newIdTokenEntity = new OAuth2AccessTokenEntity(); - - // copy over all existing claims - JWTClaimsSet claims = new JWTClaimsSet(idToken.getJWTClaimsSet()); - - if (client instanceof ClientDetailsEntity) { - - ClientDetailsEntity clientEntity = (ClientDetailsEntity) client; - - // update expiration and issued-at claims - if (clientEntity.getIdTokenValiditySeconds() != null) { - Date expiration = new Date(System.currentTimeMillis() + (clientEntity.getIdTokenValiditySeconds() * 1000L)); - claims.setExpirationTime(expiration); - newIdTokenEntity.setExpiration(expiration); - } - - } else { - //This should never happen - logger.fatal("SEVERE: Client is not an instance of OAuth2AccessTokenEntity."); - throw new BadCredentialsException("SEVERE: Client is not an instance of ClientDetailsEntity; JwtAssertionTokenGranter cannot process this request."); - } - - claims.setIssueTime(new Date()); - - - SignedJWT newIdToken = new SignedJWT((JWSHeader) idToken.getHeader(), claims); - jwtService.signJwt(newIdToken); - - newIdTokenEntity.setJwt(newIdToken); - newIdTokenEntity.setAuthenticationHolder(incomingToken.getAuthenticationHolder()); - newIdTokenEntity.setScope(incomingToken.getScope()); - newIdTokenEntity.setClient(incomingToken.getClient()); - - newIdTokenEntity = tokenServices.saveAccessToken(newIdTokenEntity); - - // attach the ID token to the access token entity - accessToken.setIdToken(newIdTokenEntity); - accessToken = tokenServices.saveAccessToken(accessToken); - - // delete the old ID token - tokenServices.revokeAccessToken(incomingToken); - - return newIdTokenEntity; - - } - } catch (ParseException e) { - logger.warn("Couldn't parse id token", e); - } - - } - - // if we got down here, we didn't actually create any tokens, so return null - - return null; - - /* - * Otherwise, process it like an access token assertion ... which we don't support yet so this is all commented out - * / - if (jwtService.validateSignature(incomingTokenValue)) { - - Jwt jwt = Jwt.parse(incomingTokenValue); - - - if (oldToken.getScope().contains("id-token")) { - // TODO: things - } - - // TODO: should any of these throw an exception instead of returning null? - JwtClaims claims = jwt.getClaims(); - if (!config.getIssuer().equals(claims.getIssuer())) { - // issuer isn't us - return null; - } - - if (!authorizationRequest.getClientId().equals(claims.getAudience())) { - // audience isn't the client - return null; - } - - Date now = new Date(); - if (!now.after(claims.getExpiration())) { - // token is expired - return null; - } - - // FIXME - // This doesn't work. We need to look up the old token, figure out its scopes and bind it appropriately. - // In the case of an ID token, we need to look up its parent access token and change the reference, and revoke the old one, and - // that's tricky. - // we might need new calls on the token services layer to handle this, and we might - // need to handle id tokens separately. - return new OAuth2Authentication(authorizationRequest, null); - - } else { - return null; // throw error?? - } - */ - - } - - - -} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/token/StructuredScopeAwareOAuth2RequestValidator.java b/openid-connect-server/src/main/java/org/mitre/oauth2/token/ScopeServiceAwareOAuth2RequestValidator.java similarity index 85% rename from openid-connect-server/src/main/java/org/mitre/oauth2/token/StructuredScopeAwareOAuth2RequestValidator.java rename to openid-connect-server/src/main/java/org/mitre/oauth2/token/ScopeServiceAwareOAuth2RequestValidator.java index c35330706c..3896dfd3a5 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/token/StructuredScopeAwareOAuth2RequestValidator.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/token/ScopeServiceAwareOAuth2RequestValidator.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.oauth2.token; @@ -30,14 +31,14 @@ import org.springframework.security.oauth2.provider.TokenRequest; /** - * + * * Validates the scopes on a request by comparing them against a client's - * allowed scopes, but allow structured scopes to function. - * + * allowed scopes, but allow custom scopes to function through the system scopes + * * @author jricher - * + * */ -public class StructuredScopeAwareOAuth2RequestValidator implements OAuth2RequestValidator { +public class ScopeServiceAwareOAuth2RequestValidator implements OAuth2RequestValidator { @Autowired private SystemScopeService scopeService; @@ -49,7 +50,7 @@ private void validateScope(Set requestedScopes, Set clientScopes if (requestedScopes != null && !requestedScopes.isEmpty()) { if (clientScopes != null && !clientScopes.isEmpty()) { if (!scopeService.scopesMatch(clientScopes, requestedScopes)) { - throw new InvalidScopeException("Invalid scope", clientScopes); + throw new InvalidScopeException("Invalid scope; requested:" + requestedScopes, clientScopes); } } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/view/TokenApiView.java b/openid-connect-server/src/main/java/org/mitre/oauth2/view/TokenApiView.java index a47304d768..cd6eed06cc 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/view/TokenApiView.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/view/TokenApiView.java @@ -1,6 +1,5 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,10 +25,12 @@ import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.openid.connect.view.HttpCodeView; import org.mitre.openid.connect.view.JsonEntityView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.web.servlet.view.AbstractView; @@ -43,86 +44,90 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; -@Component("tokenApiView") +@Component(TokenApiView.VIEWNAME) public class TokenApiView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(JsonEntityView.class); + public static final String VIEWNAME = "tokenApiView"; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(TokenApiView.class); private Gson gson = new GsonBuilder() - .setExclusionStrategies(new ExclusionStrategy() { + .setExclusionStrategies(new ExclusionStrategy() { - @Override - public boolean shouldSkipField(FieldAttributes f) { - return false; - } + @Override + public boolean shouldSkipField(FieldAttributes f) { + return false; + } - @Override - public boolean shouldSkipClass(Class clazz) { - // skip the JPA binding wrapper - if (clazz.equals(BeanPropertyBindingResult.class)) { - return true; - } - return false; - } + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } + return false; + } - }) - .registerTypeAdapter(OAuth2AccessTokenEntity.class, new JsonSerializer() { + }) + .registerTypeAdapter(OAuth2AccessTokenEntity.class, new JsonSerializer() { - @Override - public JsonElement serialize(OAuth2AccessTokenEntity src, - Type typeOfSrc, JsonSerializationContext context) { + @Override + public JsonElement serialize(OAuth2AccessTokenEntity src, + Type typeOfSrc, JsonSerializationContext context) { - JsonObject o = new JsonObject(); + JsonObject o = new JsonObject(); - o.addProperty("value", src.getValue()); - o.addProperty("id", src.getId()); - o.addProperty("idTokenId", src.getIdToken() != null ? src.getIdToken().getId() : null); - o.addProperty("refreshTokenId", src.getRefreshToken() != null ? src.getRefreshToken().getId() : null); + o.addProperty("value", src.getValue()); + o.addProperty("id", src.getId()); + o.addProperty("refreshTokenId", src.getRefreshToken() != null ? src.getRefreshToken().getId() : null); - o.add("scopes", context.serialize(src.getScope())); + o.add("scopes", context.serialize(src.getScope())); - o.addProperty("clientId", src.getClient().getClientId()); - o.addProperty("userId", src.getAuthenticationHolder().getAuthentication().getName()); + o.addProperty("clientId", src.getClient().getClientId()); + o.addProperty("userId", src.getAuthenticationHolder().getAuthentication().getName()); - o.add("expiration", context.serialize(src.getExpiration())); + o.add("expiration", context.serialize(src.getExpiration())); - return o; - } + return o; + } - }) - .registerTypeAdapter(OAuth2RefreshTokenEntity.class, new JsonSerializer() { + }) + .registerTypeAdapter(OAuth2RefreshTokenEntity.class, new JsonSerializer() { - @Override - public JsonElement serialize(OAuth2RefreshTokenEntity src, - Type typeOfSrc, JsonSerializationContext context) { - JsonObject o = new JsonObject(); + @Override + public JsonElement serialize(OAuth2RefreshTokenEntity src, + Type typeOfSrc, JsonSerializationContext context) { + JsonObject o = new JsonObject(); - o.addProperty("value", src.getValue()); - o.addProperty("id", src.getId()); + o.addProperty("value", src.getValue()); + o.addProperty("id", src.getId()); - o.add("scopes", context.serialize(src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope())); + o.add("scopes", context.serialize(src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope())); - o.addProperty("clientId", src.getClient().getClientId()); - o.addProperty("userId", src.getAuthenticationHolder().getAuthentication().getName()); + o.addProperty("clientId", src.getClient().getClientId()); + o.addProperty("userId", src.getAuthenticationHolder().getAuthentication().getName()); - o.add("expiration", context.serialize(src.getExpiration())); + o.add("expiration", context.serialize(src.getExpiration())); - return o; - } + return o; + } - }) - .serializeNulls() - .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .create(); + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); if (code == null) { code = HttpStatus.OK; // default to 200 } @@ -132,7 +137,7 @@ protected void renderMergedOutputModel(Map model, HttpServletReq try { Writer out = response.getWriter(); - Object obj = model.get("entity"); + Object obj = model.get(JsonEntityView.ENTITY); gson.toJson(obj, out); } catch (IOException e) { diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/view/TokenIntrospectionView.java b/openid-connect-server/src/main/java/org/mitre/oauth2/view/TokenIntrospectionView.java deleted file mode 100644 index bcce2667d5..0000000000 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/view/TokenIntrospectionView.java +++ /dev/null @@ -1,141 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.oauth2.view; - -import java.io.IOException; -import java.io.Writer; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.swing.text.DateFormatter; - -import org.mitre.oauth2.model.OAuth2AccessTokenEntity; -import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; -import org.mitre.openid.connect.model.UserInfo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.view.AbstractView; - -import com.google.common.base.Joiner; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; - -@Component("tokenIntrospection") -public class TokenIntrospectionView extends AbstractView { - - private static Logger logger = LoggerFactory.getLogger(TokenIntrospectionView.class); - - private static DateFormatter isoDateFormatter = new DateFormatter(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")); - - private Gson gson = new GsonBuilder().create(); - - @Override - protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - - response.setContentType("application/json"); - - Writer out; - - try { - - out = response.getWriter(); - UserInfo user = (UserInfo)model.get("user"); - Object obj = model.get("token"); - if (obj instanceof OAuth2AccessTokenEntity) { - gson.toJson(renderAccessToken((OAuth2AccessTokenEntity)obj, user), out); - } else if (obj instanceof OAuth2RefreshTokenEntity) { - gson.toJson(renderRefreshToken((OAuth2RefreshTokenEntity)obj, user), out); - } else { - throw new IOException("Couldn't find a valid entity to render"); - } - - } catch (IOException e) { - - logger.error("IOException occurred in TokenIntrospectionView.java: ", e); - - } - - } - - private JsonObject renderAccessToken(OAuth2AccessTokenEntity src, UserInfo user) { - JsonObject token = new JsonObject(); - - token.addProperty("active", true); - - token.addProperty("scope", Joiner.on(" ").join(src.getScope())); - - if (src.getExpiration() != null) { - try { - token.addProperty("exp", isoDateFormatter.valueToString(src.getExpiration())); - } catch (ParseException e) { - logger.error("Problem formatting expiration date: " + src.getExpiration(), e); - } - } - - if (user != null) { - // if we have a UserInfo, use that for the subject - token.addProperty("sub", user.getSub()); - token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName()); - } else { - // otherwise, use the authentication's username - token.addProperty("sub", src.getAuthenticationHolder().getAuthentication().getName()); - token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName()); - } - - token.addProperty("client_id", src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getClientId()); - - token.addProperty("token_type", src.getTokenType()); - - return token; - } - - private JsonObject renderRefreshToken(OAuth2RefreshTokenEntity src, UserInfo user) { - JsonObject token = new JsonObject(); - - token.addProperty("active", true); - - token.addProperty("scope", Joiner.on(" ").join(src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope())); - - if (src.getExpiration() != null) { - try { - token.addProperty("exp", isoDateFormatter.valueToString(src.getExpiration())); - } catch (ParseException e) { - logger.error("Problem formatting expiration date: " + src.getExpiration(), e); - } - } - - if (user != null) { - // if we have a UserInfo, use that for the subject - token.addProperty("sub", user.getSub()); - token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName()); - } else { - // otherwise, use the authentication's username - token.addProperty("sub", src.getAuthenticationHolder().getAuthentication().getName()); - token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName()); - } - - token.addProperty("client_id", src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getClientId()); - - return token; - } - -} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/AuthenticationUtilities.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/AuthenticationUtilities.java new file mode 100644 index 0000000000..ee56889cc0 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/AuthenticationUtilities.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; + +import com.google.common.collect.ImmutableSet; + +/** + * + * Utility class to enforce OAuth scopes in authenticated requests. + * + * @author jricher + * + */ +public abstract class AuthenticationUtilities { + + /** + * Makes sure the authentication contains the given scope, throws an exception otherwise + * @param auth the authentication object to check + * @param scope the scope to look for + * @throws InsufficientScopeException if the authentication does not contain that scope + */ + public static void ensureOAuthScope(Authentication auth, String scope) { + // if auth is OAuth, make sure we've got the right scope + if (auth instanceof OAuth2Authentication) { + OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) auth; + if (oAuth2Authentication.getOAuth2Request().getScope() == null + || !oAuth2Authentication.getOAuth2Request().getScope().contains(scope)) { + throw new InsufficientScopeException("Insufficient scope", ImmutableSet.of(scope)); + } + } + } + + /** + * Check to see if the given auth object has ROLE_ADMIN assigned to it or not + * @param auth + * @return + */ + public static boolean isAdmin(Authentication auth) { + for (GrantedAuthority grantedAuthority : auth.getAuthorities()) { + if (grantedAuthority.getAuthority().equals("ROLE_ADMIN")) { + return true; + } + } + return false; + } + + + public static boolean hasRole(Authentication auth, String role) { + for (GrantedAuthority grantedAuthority : auth.getAuthorities()) { + if (grantedAuthority.getAuthority().equals(role)) { + return true; + } + } + return false; + + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/CorsFilter.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/CorsFilter.java index 015d652bc4..da2aa69d12 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/CorsFilter.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/CorsFilter.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.oauth2.web; @@ -30,11 +31,11 @@ import org.springframework.web.filter.OncePerRequestFilter; /** - * + * * Implements Cross-Origin Resource Sharing (CORS) headers. This filter adds the CORS * headers to all requests that pass through it, and as such it should be used only * on endpoints that require CORS support. - * + * * @author jricher * */ diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/DeviceEndpoint.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/DeviceEndpoint.java new file mode 100644 index 0000000000..9c54c9f073 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/DeviceEndpoint.java @@ -0,0 +1,307 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.web; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.servlet.http.HttpSession; + +import org.apache.http.client.utils.URIBuilder; +import org.mitre.oauth2.exception.DeviceCodeCreationException; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.DeviceCode; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.DeviceCodeService; +import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.oauth2.token.DeviceTokenGranter; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.oauth2.common.util.OAuth2Utils; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import com.google.common.collect.Sets; + +/** + * Implements https://tools.ietf.org/html/draft-ietf-oauth-device-flow + * + * @see DeviceTokenGranter + * + * @author jricher + * + */ +@Controller +public class DeviceEndpoint { + + public static final String URL = "devicecode"; + public static final String USER_URL = "device"; + + public static final Logger logger = LoggerFactory.getLogger(DeviceEndpoint.class); + + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private SystemScopeService scopeService; + + @Autowired + private ConfigurationPropertiesBean config; + + @Autowired + private DeviceCodeService deviceCodeService; + + @Autowired + private OAuth2RequestFactory oAuth2RequestFactory; + + @RequestMapping(value = "/" + URL, method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public String requestDeviceCode(@RequestParam("client_id") String clientId, @RequestParam(name="scope", required=false) String scope, Map parameters, ModelMap model) { + + ClientDetailsEntity client; + try { + client = clientService.loadClientByClientId(clientId); + + // make sure this client can do the device flow + + Collection authorizedGrantTypes = client.getAuthorizedGrantTypes(); + if (authorizedGrantTypes != null && !authorizedGrantTypes.isEmpty() + && !authorizedGrantTypes.contains(DeviceTokenGranter.GRANT_TYPE)) { + throw new InvalidClientException("Unauthorized grant type: " + DeviceTokenGranter.GRANT_TYPE); + } + + } catch (IllegalArgumentException e) { + logger.error("IllegalArgumentException was thrown when attempting to load client", e); + model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + return HttpCodeView.VIEWNAME; + } + + if (client == null) { + logger.error("could not find client " + clientId); + model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + // make sure the client is allowed to ask for those scopes + Set requestedScopes = OAuth2Utils.parseParameterList(scope); + Set allowedScopes = client.getScope(); + + if (!scopeService.scopesMatch(allowedScopes, requestedScopes)) { + // client asked for scopes it can't have + logger.error("Client asked for " + requestedScopes + " but is allowed " + allowedScopes); + model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + model.put(JsonErrorView.ERROR, "invalid_scope"); + return JsonErrorView.VIEWNAME; + } + + // if we got here the request is legit + + try { + DeviceCode dc = deviceCodeService.createNewDeviceCode(requestedScopes, client, parameters); + + Map response = new HashMap<>(); + response.put("device_code", dc.getDeviceCode()); + response.put("user_code", dc.getUserCode()); + response.put("verification_uri", config.getIssuer() + USER_URL); + if (client.getDeviceCodeValiditySeconds() != null) { + response.put("expires_in", client.getDeviceCodeValiditySeconds()); + } + + if (config.isAllowCompleteDeviceCodeUri()) { + URI verificationUriComplete = new URIBuilder(config.getIssuer() + USER_URL) + .addParameter("user_code", dc.getUserCode()) + .build(); + + response.put("verification_uri_complete", verificationUriComplete.toString()); + } + + model.put(JsonEntityView.ENTITY, response); + + + return JsonEntityView.VIEWNAME; + } catch (DeviceCodeCreationException dcce) { + + model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + model.put(JsonErrorView.ERROR, dcce.getError()); + model.put(JsonErrorView.ERROR_MESSAGE, dcce.getMessage()); + + return JsonErrorView.VIEWNAME; + } catch (URISyntaxException use) { + logger.error("unable to build verification_uri_complete due to wrong syntax of uri components"); + model.put(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); + + return HttpCodeView.VIEWNAME; + } + + } + + @PreAuthorize("hasRole('ROLE_USER')") + @RequestMapping(value = "/" + USER_URL, method = RequestMethod.GET) + public String requestUserCode(@RequestParam(value = "user_code", required = false) String userCode, ModelMap model, HttpSession session) { + + if (!config.isAllowCompleteDeviceCodeUri() || userCode == null) { + // if we don't allow the complete URI or we didn't get a user code on the way in, + // print out a page that asks the user to enter their user code + // user must be logged in + return "requestUserCode"; + } else { + + // complete verification uri was used, we received user code directly + // skip requesting code page + // user must be logged in + return readUserCode(userCode, model, session); + } + } + + @PreAuthorize("hasRole('ROLE_USER')") + @RequestMapping(value = "/" + USER_URL + "/verify", method = RequestMethod.POST) + public String readUserCode(@RequestParam("user_code") String userCode, ModelMap model, HttpSession session) { + + // look up the request based on the user code + DeviceCode dc = deviceCodeService.lookUpByUserCode(userCode); + + // we couldn't find the device code + if (dc == null) { + model.addAttribute("error", "noUserCode"); + return "requestUserCode"; + } + + // make sure the code hasn't expired yet + if (dc.getExpiration() != null && dc.getExpiration().before(new Date())) { + model.addAttribute("error", "expiredUserCode"); + return "requestUserCode"; + } + + // make sure the device code hasn't already been approved + if (dc.isApproved()) { + model.addAttribute("error", "userCodeAlreadyApproved"); + return "requestUserCode"; + } + + ClientDetailsEntity client = clientService.loadClientByClientId(dc.getClientId()); + + model.put("client", client); + model.put("dc", dc); + + // pre-process the scopes + Set scopes = scopeService.fromStrings(dc.getScope()); + + Set sortedScopes = new LinkedHashSet<>(scopes.size()); + Set systemScopes = scopeService.getAll(); + + // sort scopes for display based on the inherent order of system scopes + for (SystemScope s : systemScopes) { + if (scopes.contains(s)) { + sortedScopes.add(s); + } + } + + // add in any scopes that aren't system scopes to the end of the list + sortedScopes.addAll(Sets.difference(scopes, systemScopes)); + + model.put("scopes", sortedScopes); + + AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(dc.getRequestParameters()); + + session.setAttribute("authorizationRequest", authorizationRequest); + session.setAttribute("deviceCode", dc); + + return "approveDevice"; + } + + @PreAuthorize("hasRole('ROLE_USER')") + @RequestMapping(value = "/" + USER_URL + "/approve", method = RequestMethod.POST) + public String approveDevice(@RequestParam("user_code") String userCode, @RequestParam(value = "user_oauth_approval") Boolean approve, ModelMap model, Authentication auth, HttpSession session) { + + AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute("authorizationRequest"); + DeviceCode dc = (DeviceCode) session.getAttribute("deviceCode"); + + // make sure the form that was submitted is the one that we were expecting + if (!dc.getUserCode().equals(userCode)) { + model.addAttribute("error", "userCodeMismatch"); + return "requestUserCode"; + } + + // make sure the code hasn't expired yet + if (dc.getExpiration() != null && dc.getExpiration().before(new Date())) { + model.addAttribute("error", "expiredUserCode"); + return "requestUserCode"; + } + + ClientDetailsEntity client = clientService.loadClientByClientId(dc.getClientId()); + + model.put("client", client); + + // user did not approve + if (!approve) { + model.addAttribute("approved", false); + return "deviceApproved"; + } + + // create an OAuth request for storage + OAuth2Request o2req = oAuth2RequestFactory.createOAuth2Request(authorizationRequest); + OAuth2Authentication o2Auth = new OAuth2Authentication(o2req, auth); + + DeviceCode approvedCode = deviceCodeService.approveDeviceCode(dc, o2Auth); + + // pre-process the scopes + Set scopes = scopeService.fromStrings(dc.getScope()); + + Set sortedScopes = new LinkedHashSet<>(scopes.size()); + Set systemScopes = scopeService.getAll(); + + // sort scopes for display based on the inherent order of system scopes + for (SystemScope s : systemScopes) { + if (scopes.contains(s)) { + sortedScopes.add(s); + } + } + + // add in any scopes that aren't system scopes to the end of the list + sortedScopes.addAll(Sets.difference(scopes, systemScopes)); + + model.put("scopes", sortedScopes); + model.put("approved", true); + + return "deviceApproved"; + } +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java index 14923e5252..7725a54030 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java @@ -1,22 +1,26 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.web; -import java.security.Principal; +import static org.mitre.oauth2.web.AuthenticationUtilities.ensureOAuthScope; + +import java.util.Collection; +import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -24,16 +28,22 @@ import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; import org.mitre.oauth2.service.ClientDetailsEntityService; -import org.mitre.oauth2.service.IntrospectionAuthorizer; +import org.mitre.oauth2.service.IntrospectionResultAssembler; import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.mitre.oauth2.service.SystemScopeService; import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.service.UserInfoService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ResourceSetService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @@ -45,6 +55,11 @@ @Controller public class IntrospectionEndpoint { + /** + * + */ + public static final String URL = "introspect"; + @Autowired private OAuth2TokenEntityService tokenServices; @@ -52,12 +67,18 @@ public class IntrospectionEndpoint { private ClientDetailsEntityService clientService; @Autowired - private IntrospectionAuthorizer introspectionAuthorizer; - + private IntrospectionResultAssembler introspectionResultAssembler; + @Autowired private UserInfoService userInfoService; - private static Logger logger = LoggerFactory.getLogger(IntrospectionEndpoint.class); + @Autowired + private ResourceSetService resourceSetService; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(IntrospectionEndpoint.class); public IntrospectionEndpoint() { @@ -67,87 +88,124 @@ public IntrospectionEndpoint(OAuth2TokenEntityService tokenServices) { this.tokenServices = tokenServices; } - @PreAuthorize("hasRole('ROLE_CLIENT')") - @RequestMapping("/introspect") + @RequestMapping("/" + URL) public String verify(@RequestParam("token") String tokenValue, - @RequestParam(value = "resource_id", required = false) String resourceId, @RequestParam(value = "token_type_hint", required = false) String tokenType, - Principal p, Model model) { + Authentication auth, Model model) { + + ClientDetailsEntity authClient = null; + Set authScopes = new HashSet<>(); + + if (auth instanceof OAuth2Authentication) { + // the client authenticated with OAuth, do our UMA checks + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + + // get out the client that was issued the access token (not the token being introspected) + OAuth2Authentication o2a = (OAuth2Authentication) auth; + + String authClientId = o2a.getOAuth2Request().getClientId(); + authClient = clientService.loadClientByClientId(authClientId); + + // the owner is the user who authorized the token in the first place + String ownerId = o2a.getUserAuthentication().getName(); + + authScopes.addAll(authClient.getScope()); + + // UMA style clients also get a subset of scopes of all the resource sets they've registered + Collection resourceSets = resourceSetService.getAllForOwnerAndClient(ownerId, authClientId); + + // collect all the scopes + for (ResourceSet rs : resourceSets) { + authScopes.addAll(rs.getScopes()); + } + + } else { + // the client authenticated directly, make sure it's got the right access + String authClientId = auth.getName(); // direct authentication puts the client_id into the authentication's name field + authClient = clientService.loadClientByClientId(authClientId); + + // directly authenticated clients get a subset of any scopes that they've registered for + authScopes.addAll(authClient.getScope()); + + if (!AuthenticationUtilities.hasRole(auth, "ROLE_CLIENT") + || !authClient.isAllowIntrospection()) { + + // this client isn't allowed to do direct introspection + + logger.error("Client " + authClient.getClientId() + " is not allowed to call introspection endpoint"); + model.addAttribute("code", HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + + } + + } + + // by here we're allowed to introspect, now we need to look up the token in our token stores + + // first make sure the token is there if (Strings.isNullOrEmpty(tokenValue)) { logger.error("Verify failed; token value is null"); Map entity = ImmutableMap.of("active", Boolean.FALSE); - model.addAttribute("entity", entity); - return "jsonEntityView"; + model.addAttribute(JsonEntityView.ENTITY, entity); + return JsonEntityView.VIEWNAME; } - // clientID is the principal name in the authentication - String clientId = p.getName(); - ClientDetailsEntity authClient = clientService.loadClientByClientId(clientId); - - ClientDetailsEntity tokenClient = null; - Set scopes = null; - Object token = null; - UserInfo user = null; + OAuth2AccessTokenEntity accessToken = null; + OAuth2RefreshTokenEntity refreshToken = null; + ClientDetailsEntity tokenClient; + UserInfo user; try { // check access tokens first (includes ID tokens) - OAuth2AccessTokenEntity access = tokenServices.readAccessToken(tokenValue); + accessToken = tokenServices.readAccessToken(tokenValue); - tokenClient = access.getClient(); - scopes = access.getScope(); + tokenClient = accessToken.getClient(); - token = access; + // get the user information of the user that authorized this token in the first place + String userName = accessToken.getAuthenticationHolder().getAuthentication().getName(); + user = userInfoService.getByUsernameAndClientId(userName, tokenClient.getClientId()); - user = userInfoService.getByUsernameAndClientId(access.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId()); - } catch (InvalidTokenException e) { - logger.error("Verify failed; Invalid access token. Checking refresh token.", e); + logger.info("Invalid access token. Checking refresh token."); try { // check refresh tokens next - OAuth2RefreshTokenEntity refresh = tokenServices.getRefreshToken(tokenValue); + refreshToken = tokenServices.getRefreshToken(tokenValue); - tokenClient = refresh.getClient(); - scopes = refresh.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope(); + tokenClient = refreshToken.getClient(); - user = userInfoService.getByUsernameAndClientId(refresh.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId()); - - token = refresh; + // get the user information of the user that authorized this token in the first place + String userName = refreshToken.getAuthenticationHolder().getAuthentication().getName(); + user = userInfoService.getByUsernameAndClientId(userName, tokenClient.getClientId()); } catch (InvalidTokenException e2) { - logger.error("Verify failed; Invalid refresh token", e2); - Map entity = ImmutableMap.of("active", Boolean.FALSE); - model.addAttribute("entity", entity); - return "jsonEntityView"; + logger.error("Invalid refresh token"); + Map entity = ImmutableMap.of(IntrospectionResultAssembler.ACTIVE, Boolean.FALSE); + model.addAttribute(JsonEntityView.ENTITY, entity); + return JsonEntityView.VIEWNAME; } } - if (tokenClient != null && authClient != null) { - if (authClient.isAllowIntrospection()) { - if (introspectionAuthorizer.isIntrospectionPermitted(authClient, tokenClient, scopes)) { - // if it's a valid token, we'll print out information on it - model.addAttribute("token", token); - model.addAttribute("user", user); - return "tokenIntrospection"; - } else { - logger.error("Verify failed; client configuration or scope don't permit token introspection"); - model.addAttribute("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; - } - } else { - logger.error("Verify failed; client " + clientId + " is not allowed to call introspection endpoint"); - model.addAttribute("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; - } + // if it's a valid token, we'll print out information on it + + if (accessToken != null) { + Map entity = introspectionResultAssembler.assembleFrom(accessToken, user, authScopes); + model.addAttribute(JsonEntityView.ENTITY, entity); + } else if (refreshToken != null) { + Map entity = introspectionResultAssembler.assembleFrom(refreshToken, user, authScopes); + model.addAttribute(JsonEntityView.ENTITY, entity); } else { - // This is a bad error -- I think it means we have a token outstanding that doesn't map to a client? - logger.error("Verify failed; client " + clientId + " not found."); - model.addAttribute("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; + // no tokens were found (we shouldn't get here) + logger.error("Verify failed; Invalid access/refresh token"); + Map entity = ImmutableMap.of(IntrospectionResultAssembler.ACTIVE, Boolean.FALSE); + model.addAttribute(JsonEntityView.ENTITY, entity); + return JsonEntityView.VIEWNAME; } + return JsonEntityView.VIEWNAME; + } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/OAuth2ExceptionHandler.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/OAuth2ExceptionHandler.java new file mode 100644 index 0000000000..2a361cffde --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/OAuth2ExceptionHandler.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +/** + * Controller helper that handles OAuth2 exceptions and propagates them as JSON errors. + * + * @author jricher + * + */ +@ControllerAdvice +public class OAuth2ExceptionHandler { + private static final Logger logger = LoggerFactory.getLogger(OAuth2ExceptionHandler.class); + + @Autowired + private WebResponseExceptionTranslator providerExceptionHandler; + + @ExceptionHandler(OAuth2Exception.class) + public ResponseEntity handleException(Exception e) throws Exception { + logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage()); + return providerExceptionHandler.translate(e); + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java index 39106cb1bd..29c9a1419e 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java @@ -1,24 +1,29 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.web; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT_SEPARATOR; + +import java.net.URISyntaxException; import java.security.Principal; import java.util.Date; import java.util.HashMap; @@ -27,6 +32,7 @@ import java.util.Map; import java.util.Set; +import org.apache.http.client.utils.URIBuilder; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.service.ClientDetailsEntityService; @@ -35,6 +41,7 @@ import org.mitre.openid.connect.service.ScopeClaimTranslationService; import org.mitre.openid.connect.service.StatsService; import org.mitre.openid.connect.service.UserInfoService; +import org.mitre.openid.connect.view.HttpCodeView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -42,6 +49,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; @@ -61,6 +69,7 @@ @SessionAttributes("authorizationRequest") public class OAuthConfirmationController { + @Autowired private ClientDetailsEntityService clientService; @@ -76,7 +85,13 @@ public class OAuthConfirmationController { @Autowired private StatsService statsService; - private static Logger logger = LoggerFactory.getLogger(OAuthConfirmationController.class); + @Autowired + private RedirectResolver redirectResolver; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(OAuthConfirmationController.class); public OAuthConfirmationController() { @@ -88,40 +103,53 @@ public OAuthConfirmationController(ClientDetailsEntityService clientService) { @PreAuthorize("hasRole('ROLE_USER')") @RequestMapping("/oauth/confirm_access") - public String confimAccess(Map model, @ModelAttribute("authorizationRequest") AuthorizationRequest authRequest, - Principal p) { + public String confirmAccess(Map model, Principal p) { + AuthorizationRequest authRequest = (AuthorizationRequest) model.get("authorizationRequest"); // Check the "prompt" parameter to see if we need to do special processing - String prompt = (String)authRequest.getExtensions().get("prompt"); - List prompts = Splitter.on(" ").splitToList(Strings.nullToEmpty(prompt)); - if (prompts.contains("none")) { - // we're not supposed to prompt, so "return an error" - logger.info("Client requested no prompt, returning 403 from confirmation endpoint"); - model.put("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; - } - - //AuthorizationRequest clientAuth = (AuthorizationRequest) model.remove("authorizationRequest"); - + String prompt = (String)authRequest.getExtensions().get(PROMPT); + List prompts = Splitter.on(PROMPT_SEPARATOR).splitToList(Strings.nullToEmpty(prompt)); ClientDetailsEntity client = null; try { client = clientService.loadClientByClientId(authRequest.getClientId()); } catch (OAuth2Exception e) { logger.error("confirmAccess: OAuth2Exception was thrown when attempting to load client", e); - model.put("code", HttpStatus.BAD_REQUEST); - return "httpCodeView"; + model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + return HttpCodeView.VIEWNAME; } catch (IllegalArgumentException e) { logger.error("confirmAccess: IllegalArgumentException was thrown when attempting to load client", e); - model.put("code", HttpStatus.BAD_REQUEST); - return "httpCodeView"; + model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + return HttpCodeView.VIEWNAME; } if (client == null) { logger.error("confirmAccess: could not find client " + authRequest.getClientId()); - model.put("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; + model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + if (prompts.contains("none")) { + // if we've got a redirect URI then we'll send it + + String url = redirectResolver.resolveRedirect(authRequest.getRedirectUri(), client); + + try { + URIBuilder uriBuilder = new URIBuilder(url); + + uriBuilder.addParameter("error", "interaction_required"); + if (!Strings.isNullOrEmpty(authRequest.getState())) { + uriBuilder.addParameter("state", authRequest.getState()); // copy the state parameter if one was given + } + + return "redirect:" + uriBuilder.toString(); + + } catch (URISyntaxException e) { + logger.error("Can't build redirect URI for prompt=none, sending error instead", e); + model.put("code", HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + } } model.put("auth_request", authRequest); @@ -135,7 +163,7 @@ public String confimAccess(Map model, @ModelAttribute("authoriza // pre-process the scopes Set scopes = scopeService.fromStrings(authRequest.getScope()); - Set sortedScopes = new LinkedHashSet(scopes.size()); + Set sortedScopes = new LinkedHashSet<>(scopes.size()); Set systemScopes = scopeService.getAll(); // sort scopes for display based on the inherent order of system scopes @@ -152,27 +180,29 @@ public String confimAccess(Map model, @ModelAttribute("authoriza // get the userinfo claims for each scope UserInfo user = userInfoService.getByUsername(p.getName()); - JsonObject userJson = user.toJson(); - Map> claimsForScopes = new HashMap>(); - - for (SystemScope systemScope : sortedScopes) { - Map claimValues = new HashMap(); - - Set claims = scopeClaimTranslationService.getClaimsForScope(systemScope.getValue()); - for (String claim : claims) { - if (userJson.has(claim) && userJson.get(claim).isJsonPrimitive()) { - // TODO: this skips the address claim - claimValues.put(claim, userJson.get(claim).getAsString()); + Map> claimsForScopes = new HashMap<>(); + if (user != null) { + JsonObject userJson = user.toJson(); + + for (SystemScope systemScope : sortedScopes) { + Map claimValues = new HashMap<>(); + + Set claims = scopeClaimTranslationService.getClaimsForScope(systemScope.getValue()); + for (String claim : claims) { + if (userJson.has(claim) && userJson.get(claim).isJsonPrimitive()) { + // TODO: this skips the address claim + claimValues.put(claim, userJson.get(claim).getAsString()); + } } - } - claimsForScopes.put(systemScope.getValue(), claimValues); + claimsForScopes.put(systemScope.getValue(), claimValues); + } } model.put("claims", claimsForScopes); // client stats - Integer count = statsService.getCountForClientId(client.getId()); + Integer count = statsService.getCountForClientId(client.getClientId()).getApprovedSiteCount(); model.put("count", count); @@ -183,18 +213,14 @@ public String confimAccess(Map model, @ModelAttribute("authoriza } // if the client is over a week old and has more than one registration, don't give such a big warning - // instead, tag as "Generally Recognized As Safe (gras) - Date lastWeek = new Date(System.currentTimeMillis() + (60 * 60 * 24 * 7 * 1000)); - //Date lastWeek = new Date(System.currentTimeMillis() - (60 * 60 * 24 * 7 * 1000)); + // instead, tag as "Generally Recognized As Safe" (gras) + Date lastWeek = new Date(System.currentTimeMillis() - (60 * 60 * 24 * 7 * 1000)); if (count > 1 && client.getCreatedAt() != null && client.getCreatedAt().before(lastWeek)) { model.put("gras", true); } else { model.put("gras", false); } - // inject a random value for CSRF purposes - model.put("csrf", authRequest.getExtensions().get("csrf")); - return "approve"; } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java index 7786e02bb9..dd202fe9f7 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java @@ -1,34 +1,39 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.web; -import java.security.Principal; +import static org.mitre.oauth2.web.AuthenticationUtilities.ensureOAuthScope; +import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.openid.connect.view.HttpCodeView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.provider.OAuth2Authentication; -import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @@ -37,40 +42,69 @@ @Controller public class RevocationEndpoint { @Autowired - OAuth2TokenEntityService tokenServices; + private ClientDetailsEntityService clientService; - private static Logger logger = LoggerFactory.getLogger(RevocationEndpoint.class); + @Autowired + private OAuth2TokenEntityService tokenServices; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(RevocationEndpoint.class); + + public static final String URL = "revoke"; @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") - @RequestMapping("/revoke") - public String revoke(@RequestParam("token") String tokenValue, @RequestParam(value = "token_type_hint", required = false) String tokenType, Principal principal, Model model) { + @RequestMapping("/" + URL) + public String revoke(@RequestParam("token") String tokenValue, @RequestParam(value = "token_type_hint", required = false) String tokenType, Authentication auth, Model model) { // This is the token as passed in from OAuth (in case we need it some day) //OAuth2AccessTokenEntity tok = tokenServices.getAccessToken((OAuth2Authentication) principal); - OAuth2Request authRequest = null; - if (principal instanceof OAuth2Authentication) { - // if the client is acting on its own behalf (the common case), pull out the client authorization request - authRequest = ((OAuth2Authentication) principal).getOAuth2Request(); + ClientDetailsEntity authClient = null; + + if (auth instanceof OAuth2Authentication) { + // the client authenticated with OAuth, do our UMA checks + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + // get out the client that was issued the access token (not the token being revoked) + OAuth2Authentication o2a = (OAuth2Authentication) auth; + + String authClientId = o2a.getOAuth2Request().getClientId(); + authClient = clientService.loadClientByClientId(authClientId); + + // the owner is the user who authorized the token in the first place + String ownerId = o2a.getUserAuthentication().getName(); + + } else { + // the client authenticated directly, make sure it's got the right access + + String authClientId = auth.getName(); // direct authentication puts the client_id into the authentication's name field + authClient = clientService.loadClientByClientId(authClientId); + } try { // check and handle access tokens first OAuth2AccessTokenEntity accessToken = tokenServices.readAccessToken(tokenValue); - if (authRequest != null) { - // client acting on its own, make sure it owns the token - if (!accessToken.getClient().getClientId().equals(authRequest.getClientId())) { - // trying to revoke a token we don't own, throw a 403 - model.addAttribute("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; - } + + // client acting on its own, make sure it owns the token + if (!accessToken.getClient().getClientId().equals(authClient.getClientId())) { + // trying to revoke a token we don't own, throw a 403 + + logger.info("Client " + authClient.getClientId() + " tried to revoke a token owned by " + accessToken.getClient().getClientId()); + + model.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; } // if we got this far, we're allowed to do this tokenServices.revokeAccessToken(accessToken); - model.addAttribute("code", HttpStatus.OK); - return "httpCodeView"; + + logger.debug("Client " + authClient.getClientId() + " revoked access token " + tokenValue); + + model.addAttribute(HttpCodeView.CODE, HttpStatus.OK); + return HttpCodeView.VIEWNAME; } catch (InvalidTokenException e) { @@ -78,26 +112,32 @@ public String revoke(@RequestParam("token") String tokenValue, @RequestParam(val try { OAuth2RefreshTokenEntity refreshToken = tokenServices.getRefreshToken(tokenValue); - if (authRequest != null) { - // client acting on its own, make sure it owns the token - if (!refreshToken.getClient().getClientId().equals(authRequest.getClientId())) { - // trying to revoke a token we don't own, throw a 403 - model.addAttribute("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; - } + // client acting on its own, make sure it owns the token + if (!refreshToken.getClient().getClientId().equals(authClient.getClientId())) { + // trying to revoke a token we don't own, throw a 403 + + logger.info("Client " + authClient.getClientId() + " tried to revoke a token owned by " + refreshToken.getClient().getClientId()); + + model.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; } // if we got this far, we're allowed to do this tokenServices.revokeRefreshToken(refreshToken); - model.addAttribute("code", HttpStatus.OK); - return "httpCodeView"; + + logger.debug("Client " + authClient.getClientId() + " revoked access token " + tokenValue); + + model.addAttribute(HttpCodeView.CODE, HttpStatus.OK); + return HttpCodeView.VIEWNAME; } catch (InvalidTokenException e1) { // neither token type was found, simply say "OK" and be on our way. - model.addAttribute("code", HttpStatus.OK); - return "httpCodeView"; + logger.debug("Failed to revoke token " + tokenValue); + + model.addAttribute(HttpCodeView.CODE, HttpStatus.OK); + return HttpCodeView.VIEWNAME; } } } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/ScopeAPI.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/ScopeAPI.java index 7877b8e4d0..5aa6d2a3b1 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/ScopeAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/ScopeAPI.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.web; @@ -23,10 +24,15 @@ import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.openid.connect.web.RootController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -42,49 +48,54 @@ * */ @Controller -@RequestMapping("/api/scopes") +@RequestMapping("/" + ScopeAPI.URL) @PreAuthorize("hasRole('ROLE_USER')") public class ScopeAPI { + public static final String URL = RootController.API_URL + "/scopes"; + @Autowired private SystemScopeService scopeService; - private static Logger logger = LoggerFactory.getLogger(ScopeAPI.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ScopeAPI.class); private Gson gson = new Gson(); - @RequestMapping(value = "", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value = "", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getAll(ModelMap m) { Set allScopes = scopeService.getAll(); - m.put("entity", allScopes); + m.put(JsonEntityView.ENTITY, allScopes); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } - @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getScope(@PathVariable("id") Long id, ModelMap m) { SystemScope scope = scopeService.getById(id); if (scope != null) { - m.put("entity", scope); + m.put(JsonEntityView.ENTITY, scope); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } else { logger.error("getScope failed; scope not found: " + id); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "The requested scope with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested scope with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } } @PreAuthorize("hasRole('ROLE_ADMIN')") - @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = "application/json", consumes = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public String updateScope(@PathVariable("id") Long id, @RequestBody String json, ModelMap m) { SystemScope existing = scopeService.getById(id); @@ -98,31 +109,31 @@ public String updateScope(@PathVariable("id") Long id, @RequestBody String json, scope = scopeService.save(scope); - m.put("entity", scope); + m.put(JsonEntityView.ENTITY, scope); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } else { logger.error("updateScope failed; scope ids to not match: got " + existing.getId() + " and " + scope.getId()); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not update scope. Scope ids to not match: got " + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update scope. Scope ids to not match: got " + existing.getId() + " and " + scope.getId()); - return "jsonErrorView"; + return JsonErrorView.VIEWNAME; } } else { logger.error("updateScope failed; scope with id " + id + " not found."); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "Could not update scope. The scope with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update scope. The scope with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } } @PreAuthorize("hasRole('ROLE_ADMIN')") - @RequestMapping(value = "", method = RequestMethod.POST, produces = "application/json", consumes = "application/json") + @RequestMapping(value = "", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public String createScope(@RequestBody String json, ModelMap m) { SystemScope scope = gson.fromJson(json, SystemScope.class); @@ -130,24 +141,24 @@ public String createScope(@RequestBody String json, ModelMap m) { if (alreadyExists != null) { //Error, cannot save a scope with the same value as an existing one logger.error("Error: attempting to save a scope with a value that already exists: " + scope.getValue()); - m.put("code", HttpStatus.CONFLICT); - m.put("errorMessage", "A scope with value " + scope.getValue() + " already exists, please choose a different value."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.CONFLICT); + m.put(JsonErrorView.ERROR_MESSAGE, "A scope with value " + scope.getValue() + " already exists, please choose a different value."); + return JsonErrorView.VIEWNAME; } scope = scopeService.save(scope); if (scope != null && scope.getId() != null) { - m.put("entity", scope); + m.put(JsonEntityView.ENTITY, scope); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } else { logger.error("createScope failed; JSON was invalid: " + json); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not save new scope " + scope + ". The scope service failed to return a saved entity."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not save new scope " + scope + ". The scope service failed to return a saved entity."); + return JsonErrorView.VIEWNAME; } } @@ -161,14 +172,13 @@ public String deleteScope(@PathVariable("id") Long id, ModelMap m) { scopeService.remove(existing); - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } else { logger.error("deleteScope failed; scope with id " + id + " not found."); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "Could not delete scope. The requested scope with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not delete scope. The requested scope with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } } - } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/TokenAPI.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/TokenAPI.java index cd5f44a834..73fa472b47 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/TokenAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/TokenAPI.java @@ -1,31 +1,42 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.web; import java.security.Principal; +import java.util.List; import java.util.Set; +import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.mitre.oauth2.view.TokenApiView; +import org.mitre.openid.connect.service.OIDCTokenService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.openid.connect.web.RootController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -39,117 +50,197 @@ * */ @Controller -@RequestMapping("/api/tokens") +@RequestMapping("/" + TokenAPI.URL) @PreAuthorize("hasRole('ROLE_USER')") public class TokenAPI { + public static final String URL = RootController.API_URL + "/tokens"; + @Autowired private OAuth2TokenEntityService tokenService; - private static Logger logger = LoggerFactory.getLogger(TokenAPI.class); + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private OIDCTokenService oidcTokenService; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(TokenAPI.class); - @RequestMapping(value = "/access", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value = "/access", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getAllAccessTokens(ModelMap m, Principal p) { Set allTokens = tokenService.getAllAccessTokensForUser(p.getName()); - m.put("entity", allTokens); - return "tokenApiView"; + m.put(JsonEntityView.ENTITY, allTokens); + return TokenApiView.VIEWNAME; } - @RequestMapping(value = "/access/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value = "/access/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getAccessTokenById(@PathVariable("id") Long id, ModelMap m, Principal p) { OAuth2AccessTokenEntity token = tokenService.getAccessTokenById(id); if (token == null) { logger.error("getToken failed; token not found: " + id); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "The requested token with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested token with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else if (!token.getAuthenticationHolder().getAuthentication().getName().equals(p.getName())) { logger.error("getToken failed; token does not belong to principal " + p.getName()); - m.put("code", HttpStatus.FORBIDDEN); - m.put("errorMessage", "You do not have permission to view this token"); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + m.put(JsonErrorView.ERROR_MESSAGE, "You do not have permission to view this token"); + return JsonErrorView.VIEWNAME; } else { - m.put("entity", token); - return "tokenApiView"; + m.put(JsonEntityView.ENTITY, token); + return TokenApiView.VIEWNAME; } } - @RequestMapping(value = "/access/{id}", method = RequestMethod.DELETE, produces = "application/json") + @RequestMapping(value = "/access/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) public String deleteAccessTokenById(@PathVariable("id") Long id, ModelMap m, Principal p) { OAuth2AccessTokenEntity token = tokenService.getAccessTokenById(id); if (token == null) { logger.error("getToken failed; token not found: " + id); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "The requested token with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested token with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else if (!token.getAuthenticationHolder().getAuthentication().getName().equals(p.getName())) { logger.error("getToken failed; token does not belong to principal " + p.getName()); - m.put("code", HttpStatus.FORBIDDEN); - m.put("errorMessage", "You do not have permission to view this token"); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + m.put(JsonErrorView.ERROR_MESSAGE, "You do not have permission to view this token"); + return JsonErrorView.VIEWNAME; } else { tokenService.revokeAccessToken(token); - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } - @RequestMapping(value = "/refresh", method = RequestMethod.GET, produces = "application/json") + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping(value = "/client/{clientId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public String getAccessTokensByClientId(@PathVariable("clientId") String clientId, ModelMap m, Principal p) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client != null) { + List tokens = tokenService.getAccessTokensForClient(client); + m.put(JsonEntityView.ENTITY, tokens); + return TokenApiView.VIEWNAME; + } else { + // client not found + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested client with id " + clientId + " could not be found."); + return JsonErrorView.VIEWNAME; + } + + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping(value = "/registration/{clientId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public String getRegistrationTokenByClientId(@PathVariable("clientId") String clientId, ModelMap m, Principal p) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client != null) { + OAuth2AccessTokenEntity token = tokenService.getRegistrationAccessTokenForClient(client); + if (token != null) { + m.put(JsonEntityView.ENTITY, token); + return TokenApiView.VIEWNAME; + } else { + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "No registration token could be found."); + return JsonErrorView.VIEWNAME; + } + } else { + // client not found + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested client with id " + clientId + " could not be found."); + return JsonErrorView.VIEWNAME; + } + + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping(value = "/registration/{clientId}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) + public String rotateRegistrationTokenByClientId(@PathVariable("clientId") String clientId, ModelMap m, Principal p) { + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client != null) { + OAuth2AccessTokenEntity token = oidcTokenService.rotateRegistrationAccessTokenForClient(client); + token = tokenService.saveAccessToken(token); + + if (token != null) { + m.put(JsonEntityView.ENTITY, token); + return TokenApiView.VIEWNAME; + } else { + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "No registration token could be found."); + return JsonErrorView.VIEWNAME; + } + } else { + // client not found + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested client with id " + clientId + " could not be found."); + return JsonErrorView.VIEWNAME; + } + + } + + @RequestMapping(value = "/refresh", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getAllRefreshTokens(ModelMap m, Principal p) { Set allTokens = tokenService.getAllRefreshTokensForUser(p.getName()); - m.put("entity", allTokens); - return "tokenApiView"; + m.put(JsonEntityView.ENTITY, allTokens); + return TokenApiView.VIEWNAME; } - @RequestMapping(value = "/refresh/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value = "/refresh/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getRefreshTokenById(@PathVariable("id") Long id, ModelMap m, Principal p) { OAuth2RefreshTokenEntity token = tokenService.getRefreshTokenById(id); if (token == null) { logger.error("refresh token not found: " + id); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "The requested token with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested token with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else if (!token.getAuthenticationHolder().getAuthentication().getName().equals(p.getName())) { logger.error("refresh token " + id + " does not belong to principal " + p.getName()); - m.put("code", HttpStatus.FORBIDDEN); - m.put("errorMessage", "You do not have permission to view this token"); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + m.put(JsonErrorView.ERROR_MESSAGE, "You do not have permission to view this token"); + return JsonErrorView.VIEWNAME; } else { - m.put("entity", token); - return "tokenApiView"; + m.put(JsonEntityView.ENTITY, token); + return TokenApiView.VIEWNAME; } } - @RequestMapping(value = "/refresh/{id}", method = RequestMethod.DELETE, produces = "application/json") + @RequestMapping(value = "/refresh/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) public String deleteRefreshTokenById(@PathVariable("id") Long id, ModelMap m, Principal p) { OAuth2RefreshTokenEntity token = tokenService.getRefreshTokenById(id); if (token == null) { logger.error("refresh token not found: " + id); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "The requested token with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested token with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else if (!token.getAuthenticationHolder().getAuthentication().getName().equals(p.getName())) { logger.error("refresh token " + id + " does not belong to principal " + p.getName()); - m.put("code", HttpStatus.FORBIDDEN); - m.put("errorMessage", "You do not have permission to view this token"); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + m.put(JsonErrorView.ERROR_MESSAGE, "You do not have permission to view this token"); + return JsonErrorView.VIEWNAME; } else { tokenService.revokeRefreshToken(token); - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } - } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerAssertionAuthenticationToken.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerAssertionAuthenticationToken.java similarity index 66% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerAssertionAuthenticationToken.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerAssertionAuthenticationToken.java index 5814051580..bc4f9abcde 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerAssertionAuthenticationToken.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerAssertionAuthenticationToken.java @@ -1,24 +1,26 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.assertion; +import java.text.ParseException; import java.util.Collection; import org.springframework.security.authentication.AbstractAuthenticationToken; @@ -30,36 +32,48 @@ * @author jricher * */ -public class JwtBearerAssertionAuthenticationToken extends AbstractAuthenticationToken { +public class JWTBearerAssertionAuthenticationToken extends AbstractAuthenticationToken { /** - * + * */ private static final long serialVersionUID = -3138213539914074617L; - private String clientId; + private String subject; private JWT jwt; /** - * Create an unauthenticated token with the given client ID and jwt - * @param clientId + * Create an unauthenticated token with the given subject and jwt + * @param subject * @param jwt */ - public JwtBearerAssertionAuthenticationToken(String clientId, JWT jwt) { + public JWTBearerAssertionAuthenticationToken(JWT jwt) { super(null); - this.clientId = clientId; + try { + // save the subject of the JWT in case the credentials get erased later + this.subject = jwt.getJWTClaimsSet().getSubject(); + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } this.jwt = jwt; setAuthenticated(false); } /** * Create an authenticated token with the given clientID, jwt, and authorities set - * @param clientId + * @param subject * @param jwt * @param authorities */ - public JwtBearerAssertionAuthenticationToken(String clientId, JWT jwt, Collection authorities) { + public JWTBearerAssertionAuthenticationToken(JWT jwt, Collection authorities) { super(authorities); - this.clientId = clientId; + try { + // save the subject of the JWT in case the credentials get erased later + this.subject = jwt.getJWTClaimsSet().getSubject(); + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } this.jwt = jwt; setAuthenticated(true); } @@ -77,21 +91,7 @@ public Object getCredentials() { */ @Override public Object getPrincipal() { - return clientId; - } - - /** - * @return the clientId - */ - public String getClientId() { - return clientId; - } - - /** - * @param clientId the clientId to set - */ - public void setClientId(String clientId) { - this.clientId = clientId; + return subject; } /** diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerAuthenticationProvider.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerAuthenticationProvider.java similarity index 57% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerAuthenticationProvider.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerAuthenticationProvider.java index 86150d0e7f..749a8edddc 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerAuthenticationProvider.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerAuthenticationProvider.java @@ -1,30 +1,32 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.assertion; import java.text.ParseException; import java.util.Date; +import java.util.HashSet; +import java.util.Set; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; -import org.mitre.jwt.signer.service.impl.JWKSetCacheService; -import org.mitre.jwt.signer.service.impl.SymmetricCacheService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.ClientKeyCacheService; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; import org.mitre.oauth2.service.ClientDetailsEntityService; @@ -36,29 +38,32 @@ import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; +import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; /** * @author jricher * */ -public class JwtBearerAuthenticationProvider implements AuthenticationProvider { +public class JWTBearerAuthenticationProvider implements AuthenticationProvider { - private static final Logger logger = LoggerFactory.getLogger(JwtBearerAuthenticationProvider.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JWTBearerAuthenticationProvider.class); - // map of verifiers, load keys for clients - @Autowired - private JWKSetCacheService validators; + private static final GrantedAuthority ROLE_CLIENT = new SimpleGrantedAuthority("ROLE_CLIENT"); - // map of symmetric verifiers for client secrets + // map of verifiers, load keys for clients @Autowired - private SymmetricCacheService symmetricCacheService; + private ClientKeyCacheService validators; // Allow for time sync issues by having a window of X seconds. private int timeSkewAllowance = 300; @@ -77,66 +82,69 @@ public class JwtBearerAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - JwtBearerAssertionAuthenticationToken jwtAuth = (JwtBearerAssertionAuthenticationToken)authentication; + JWTBearerAssertionAuthenticationToken jwtAuth = (JWTBearerAssertionAuthenticationToken)authentication; try { - ClientDetailsEntity client = clientService.loadClientByClientId(jwtAuth.getClientId()); + ClientDetailsEntity client = clientService.loadClientByClientId(jwtAuth.getName()); JWT jwt = jwtAuth.getJwt(); - ReadOnlyJWTClaimsSet jwtClaims = jwt.getJWTClaimsSet(); + JWTClaimsSet jwtClaims = jwt.getJWTClaimsSet(); + + if (!(jwt instanceof SignedJWT)) { + throw new AuthenticationServiceException("Unsupported JWT type: " + jwt.getClass().getName()); + } // check the signature with nimbus - if (jwt instanceof SignedJWT) { - SignedJWT jws = (SignedJWT)jwt; + SignedJWT jws = (SignedJWT) jwt; - JWSAlgorithm alg = jws.getHeader().getAlgorithm(); + JWSAlgorithm alg = jws.getHeader().getAlgorithm(); - if (client.getTokenEndpointAuthSigningAlg() != null && - !client.getTokenEndpointAuthSigningAlg().equals(alg)) { - throw new InvalidClientException("Client's registered request object signing algorithm (" + client.getRequestObjectSigningAlg() + ") does not match request object's actual algorithm (" + alg.getName() + ")"); + if (client.getTokenEndpointAuthSigningAlg() != null && + !client.getTokenEndpointAuthSigningAlg().equals(alg)) { + throw new AuthenticationServiceException("Client's registered token endpoint signing algorithm (" + client.getTokenEndpointAuthSigningAlg() + + ") does not match token's actual algorithm (" + alg.getName() + ")"); + } + + if (client.getTokenEndpointAuthMethod() == null || + client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE) || + client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) || + client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST)) { + + // this client doesn't support this type of authentication + throw new AuthenticationServiceException("Client does not support this authentication method."); + + } else if ((client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY) && + (alg.equals(JWSAlgorithm.RS256) + || alg.equals(JWSAlgorithm.RS384) + || alg.equals(JWSAlgorithm.RS512) + || alg.equals(JWSAlgorithm.ES256) + || alg.equals(JWSAlgorithm.ES384) + || alg.equals(JWSAlgorithm.ES512) + || alg.equals(JWSAlgorithm.PS256) + || alg.equals(JWSAlgorithm.PS384) + || alg.equals(JWSAlgorithm.PS512))) + || (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT) && + (alg.equals(JWSAlgorithm.HS256) + || alg.equals(JWSAlgorithm.HS384) + || alg.equals(JWSAlgorithm.HS512)))) { + + // double-check the method is asymmetrical if we're in HEART mode + if (config.isHeartMode() && !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { + throw new AuthenticationServiceException("[HEART mode] Invalid authentication method"); } - if (client.getTokenEndpointAuthMethod() == null || - client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE) || - client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) || - client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST)) { - - // this client doesn't support this type of authentication - throw new AuthenticationServiceException("Client does not support this authentication method."); - - } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY) && - (alg.equals(JWSAlgorithm.RS256) - || alg.equals(JWSAlgorithm.RS384) - || alg.equals(JWSAlgorithm.RS512))) { - - JwtSigningAndValidationService validator = validators.getValidator(client.getJwksUri()); - - if (validator == null) { - throw new AuthenticationServiceException("Unable to create signature validator for client's JWKS URI: " + client.getJwksUri()); - } - - if (!validator.validateSignature(jws)) { - throw new AuthenticationServiceException("Signature did not validate for presented JWT authentication."); - } - } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT) && - (alg.equals(JWSAlgorithm.HS256) - || alg.equals(JWSAlgorithm.HS384) - || alg.equals(JWSAlgorithm.HS512))) { - - // it's HMAC, we need to make a validator based on the client secret - - JwtSigningAndValidationService validator = symmetricCacheService.getSymmetricValidtor(client); - - if (validator == null) { - throw new AuthenticationServiceException("Unable to create signature validator for client's secret: " + client.getClientSecret()); - } - - if (!validator.validateSignature(jws)) { - throw new AuthenticationServiceException("Signature did not validate for presented JWT authentication."); - } + JWTSigningAndValidationService validator = validators.getValidator(client, alg); + if (validator == null) { + throw new AuthenticationServiceException("Unable to create signature validator for client " + client + " and algorithm " + alg); } + + if (!validator.validateSignature(jws)) { + throw new AuthenticationServiceException("Signature did not validate for presented JWT authentication."); + } + } else { + throw new AuthenticationServiceException("Unable to create signature validator for method " + client.getTokenEndpointAuthMethod() + " and algorithm " + alg); } // check the issuer @@ -182,10 +190,15 @@ public Authentication authenticate(Authentication authentication) throws Authent } // IFF we managed to get all the way down here, the token is valid - return new JwtBearerAssertionAuthenticationToken(client.getClientId(), jwt, client.getAuthorities()); + + // add in the ROLE_CLIENT authority + Set authorities = new HashSet<>(client.getAuthorities()); + authorities.add(ROLE_CLIENT); + + return new JWTBearerAssertionAuthenticationToken(jwt, authorities); } catch (InvalidClientException e) { - throw new UsernameNotFoundException("Could not find client: " + jwtAuth.getClientId()); + throw new UsernameNotFoundException("Could not find client: " + jwtAuth.getName()); } catch (ParseException e) { logger.error("Failure during authentication, error was: ", e); @@ -195,11 +208,11 @@ public Authentication authenticate(Authentication authentication) throws Authent } /** - * We support {@link JwtBearerAssertionAuthenticationToken}s only. + * We support {@link JWTBearerAssertionAuthenticationToken}s only. */ @Override public boolean supports(Class authentication) { - return (JwtBearerAssertionAuthenticationToken.class.isAssignableFrom(authentication)); + return (JWTBearerAssertionAuthenticationToken.class.isAssignableFrom(authentication)); } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerClientAssertionTokenEndpointFilter.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerClientAssertionTokenEndpointFilter.java new file mode 100644 index 0000000000..e1e5ca9060 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JWTBearerClientAssertionTokenEndpointFilter.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.assertion; + +import java.io.IOException; +import java.text.ParseException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.exceptions.BadClientCredentialsException; +import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import com.google.common.base.Strings; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + +/** + * Filter to check client authentication via JWT Bearer assertions. + * + * @author jricher + * + */ +public class JWTBearerClientAssertionTokenEndpointFilter extends AbstractAuthenticationProcessingFilter { + + private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint(); + + public JWTBearerClientAssertionTokenEndpointFilter(RequestMatcher additionalMatcher) { + super(new ClientAssertionRequestMatcher(additionalMatcher)); + // If authentication fails the type is "Form" + ((OAuth2AuthenticationEntryPoint) authenticationEntryPoint).setTypeName("Form"); + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + setAuthenticationFailureHandler(new AuthenticationFailureHandler() { + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + if (exception instanceof BadCredentialsException) { + exception = new BadCredentialsException(exception.getMessage(), new BadClientCredentialsException()); + } + authenticationEntryPoint.commence(request, response, exception); + } + }); + setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() { + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + // no-op - just allow filter chain to continue to token endpoint + } + }); + } + + /** + * Pull the assertion out of the request and send it up to the auth manager for processing. + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + + // check for appropriate parameters + String assertionType = request.getParameter("client_assertion_type"); + String assertion = request.getParameter("client_assertion"); + + try { + JWT jwt = JWTParser.parse(assertion); + + String clientId = jwt.getJWTClaimsSet().getSubject(); + + Authentication authRequest = new JWTBearerAssertionAuthenticationToken(jwt); + + return this.getAuthenticationManager().authenticate(authRequest); + } catch (ParseException e) { + throw new BadCredentialsException("Invalid JWT credential: " + assertion); + } + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, + FilterChain chain, Authentication authResult) throws IOException, ServletException { + super.successfulAuthentication(request, response, chain, authResult); + chain.doFilter(request, response); + } + + private static class ClientAssertionRequestMatcher implements RequestMatcher { + + private RequestMatcher additionalMatcher; + + public ClientAssertionRequestMatcher(RequestMatcher additionalMatcher) { + this.additionalMatcher = additionalMatcher; + } + + @Override + public boolean matches(HttpServletRequest request) { + // check for appropriate parameters + String assertionType = request.getParameter("client_assertion_type"); + String assertion = request.getParameter("client_assertion"); + + if (Strings.isNullOrEmpty(assertionType) || Strings.isNullOrEmpty(assertion)) { + return false; + } else if (!assertionType.equals("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")) { + return false; + } + + return additionalMatcher.matches(request); + } + + } + + + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerClientAssertionTokenEndpointFilter.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerClientAssertionTokenEndpointFilter.java deleted file mode 100644 index 422a97c4bc..0000000000 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/assertion/JwtBearerClientAssertionTokenEndpointFilter.java +++ /dev/null @@ -1,115 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.openid.connect.assertion; - -import java.io.IOException; -import java.text.ParseException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter; - -import com.google.common.base.Strings; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTParser; - -/** - * Filter to check client authentication via JWT Bearer assertions. - * - * @author jricher - * - */ -public class JwtBearerClientAssertionTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter { - - public JwtBearerClientAssertionTokenEndpointFilter() { - super(); - } - - public JwtBearerClientAssertionTokenEndpointFilter(String path) { - super(path); - } - - /** - * Pull the assertion out of the request and send it up to the auth manager for processing. - */ - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { - - // check for appropriate parameters - String assertionType = request.getParameter("client_assertion_type"); - String assertion = request.getParameter("client_assertion"); - - try { - JWT jwt = JWTParser.parse(assertion); - - String clientId = jwt.getJWTClaimsSet().getSubject(); - - Authentication authRequest = new JwtBearerAssertionAuthenticationToken(clientId, jwt); - - return this.getAuthenticationManager().authenticate(authRequest); - } catch (ParseException e) { - throw new BadCredentialsException("Invalid JWT credential: " + assertion); - } - } - - /** - * Check to see if the "client_assertion_type" and "client_assertion" parameters are present and contain the right values. - */ - @Override - protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { - // check for appropriate parameters - String assertionType = request.getParameter("client_assertion_type"); - String assertion = request.getParameter("client_assertion"); - - if (Strings.isNullOrEmpty(assertionType) || Strings.isNullOrEmpty(assertion)) { - return false; - } else if (!assertionType.equals("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")) { - return false; - } - - - // Can't call to superclass here b/c client creds would break for lack of client_id - // return super.requiresAuthentication(request, response); - - String uri = request.getRequestURI(); - int pathParamIndex = uri.indexOf(';'); - - if (pathParamIndex > 0) { - // strip everything after the first semi-colon - uri = uri.substring(0, pathParamIndex); - } - - if ("".equals(request.getContextPath())) { - return uri.endsWith(getFilterProcessesUrl()); - } - - return uri.endsWith(request.getContextPath() + getFilterProcessesUrl()); - - } - - - - -} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/config/JsonMessageSource.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/config/JsonMessageSource.java new file mode 100644 index 0000000000..10746d3dd2 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/config/JsonMessageSource.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.config; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.AbstractMessageSource; +import org.springframework.core.io.Resource; + +import com.google.common.base.Splitter; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * @author jricher + */ +public class JsonMessageSource extends AbstractMessageSource { + + private static final Logger logger = LoggerFactory.getLogger(JsonMessageSource.class); + + private Resource baseDirectory; + + private Locale fallbackLocale = new Locale("en"); // US English is the fallback language + + private Map> languageMaps = new HashMap<>(); + + @Autowired + private ConfigurationPropertiesBean config; + + @Override + protected MessageFormat resolveCode(String code, Locale locale) { + + List langs = getLanguageMap(locale); + + String value = getValue(code, langs); + + if (value == null) { + // if we haven't found anything, try the default locale + langs = getLanguageMap(fallbackLocale); + value = getValue(code, langs); + } + + if (value == null) { + // if it's still null, return null + return null; + } else { + // otherwise format the message + return new MessageFormat(value, locale); + } + + } + + /** + * Get a value from the set of maps, taking the first match in order + * @param code + * @param langs + * @return + */ + private String getValue(String code, List langs) { + if (langs == null || langs.isEmpty()) { + // no language maps, nothing to look up + return null; + } + + for (JsonObject lang : langs) { + String value = getValue(code, lang); + if (value != null) { + // short circuit out of here if we find a match, otherwise keep going through the list + return value; + } + } + + // if we didn't find anything return null + return null; + } + + /** + * Get a value from a single map + * @param code + * @param lang + * @return + */ + private String getValue(String code, JsonObject lang) { + + // if there's no language map, nothing to look up + if (lang == null) { + return null; + } + + JsonElement e = lang; + + Iterable parts = Splitter.on('.').split(code); + Iterator it = parts.iterator(); + + String value = null; + + while (it.hasNext()) { + String p = it.next(); + if (e.isJsonObject()) { + JsonObject o = e.getAsJsonObject(); + if (o.has(p)) { + e = o.get(p); // found the next level + if (!it.hasNext()) { + // we've reached a leaf, grab it + if (e.isJsonPrimitive()) { + value = e.getAsString(); + } + } + } else { + // didn't find it, stop processing + break; + } + } else { + // didn't find it, stop processing + break; + } + } + + return value; + } + + /** + * @param locale + * @return + */ + private List getLanguageMap(Locale locale) { + + if (!languageMaps.containsKey(locale)) { + try { + List set = new ArrayList<>(); + for (String namespace : config.getLanguageNamespaces()) { + // full locale string, e.g. "en_US" + String filename = locale.getLanguage() + "_" + locale.getCountry() + File.separator + namespace + ".json"; + + Resource r = getBaseDirectory().createRelative(filename); + + if (!r.exists()) { + // fallback to language only + logger.debug("Fallback locale to language only."); + filename = locale.getLanguage() + File.separator + namespace + ".json"; + r = getBaseDirectory().createRelative(filename); + } + + logger.info("No locale loaded, trying to load from {}", r); + + JsonParser parser = new JsonParser(); + JsonObject obj = (JsonObject) parser.parse(new InputStreamReader(r.getInputStream(), "UTF-8")); + + set.add(obj); + } + languageMaps.put(locale, set); + } catch (FileNotFoundException e) { + logger.info("Unable to load locale because no messages file was found for locale {}", locale.getDisplayName()); + languageMaps.put(locale, null); + } catch (JsonIOException | JsonSyntaxException | IOException e) { + logger.error("Unable to load locale", e); + } + } + + return languageMaps.get(locale); + } + + /** + * @return the baseDirectory + */ + public Resource getBaseDirectory() { + return baseDirectory; + } + + /** + * @param baseDirectory the baseDirectory to set + */ + public void setBaseDirectory(Resource baseDirectory) { + this.baseDirectory = baseDirectory; + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/exception/ValidationException.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/exception/ValidationException.java index 3f121ff66a..85efe2c87e 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/exception/ValidationException.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/exception/ValidationException.java @@ -1,3 +1,18 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.exception; import org.springframework.http.HttpStatus; @@ -38,10 +53,10 @@ public HttpStatus getStatus() { public void setStatus(HttpStatus status) { this.status = status; } - + @Override public String toString() { return "ValidationException [error=" + error + ", errorDescription=" + errorDescription + ", status=" + status + "]"; } -} \ No newline at end of file +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/AuthorizationRequestFilter.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/AuthorizationRequestFilter.java new file mode 100644 index 0000000000..63e2390537 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/AuthorizationRequestFilter.java @@ -0,0 +1,275 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.filter; + +import static org.mitre.openid.connect.request.ConnectRequestParameters.ERROR; +import static org.mitre.openid.connect.request.ConnectRequestParameters.LOGIN_HINT; +import static org.mitre.openid.connect.request.ConnectRequestParameters.LOGIN_REQUIRED; +import static org.mitre.openid.connect.request.ConnectRequestParameters.MAX_AGE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT_LOGIN; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT_NONE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT_SEPARATOR; +import static org.mitre.openid.connect.request.ConnectRequestParameters.STATE; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.http.client.utils.URIBuilder; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.service.LoginHintExtracter; +import org.mitre.openid.connect.service.impl.RemoveLoginHintsWithHTTP; +import org.mitre.openid.connect.web.AuthenticationTimeStamper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; + +/** + * @author jricher + * + */ +@Component("authRequestFilter") +public class AuthorizationRequestFilter extends GenericFilterBean { + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(AuthorizationRequestFilter.class); + + public final static String PROMPTED = "PROMPT_FILTER_PROMPTED"; + public final static String PROMPT_REQUESTED = "PROMPT_FILTER_REQUESTED"; + + @Autowired + private OAuth2RequestFactory authRequestFactory; + + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private RedirectResolver redirectResolver; + + @Autowired(required = false) + private LoginHintExtracter loginHintExtracter = new RemoveLoginHintsWithHTTP(); + + private RequestMatcher requestMatcher = new AntPathRequestMatcher("/authorize"); + + /** + * + */ + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + HttpSession session = request.getSession(); + + // skip everything that's not an authorize URL + if (!requestMatcher.matches(request)) { + chain.doFilter(req, res); + return; + } + + try { + // we have to create our own auth request in order to get at all the parmeters appropriately + AuthorizationRequest authRequest = null; + + ClientDetailsEntity client = null; + + authRequest = authRequestFactory.createAuthorizationRequest(createRequestMap(request.getParameterMap())); + if (!Strings.isNullOrEmpty(authRequest.getClientId())) { + client = clientService.loadClientByClientId(authRequest.getClientId()); + } + + // save the login hint to the session + // but first check to see if the login hint makes any sense + String loginHint = loginHintExtracter.extractHint((String) authRequest.getExtensions().get(LOGIN_HINT)); + if (!Strings.isNullOrEmpty(loginHint)) { + session.setAttribute(LOGIN_HINT, loginHint); + } else { + session.removeAttribute(LOGIN_HINT); + } + + if (authRequest.getExtensions().get(PROMPT) != null) { + // we have a "prompt" parameter + String prompt = (String)authRequest.getExtensions().get(PROMPT); + List prompts = Splitter.on(PROMPT_SEPARATOR).splitToList(Strings.nullToEmpty(prompt)); + + if (prompts.contains(PROMPT_NONE)) { + // see if the user's logged in + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth != null) { + // user's been logged in already (by session management) + // we're OK, continue without prompting + chain.doFilter(req, res); + } else { + logger.info("Client requested no prompt"); + // user hasn't been logged in, we need to "return an error" + if (client != null && authRequest.getRedirectUri() != null) { + + // if we've got a redirect URI then we'll send it + + String url = redirectResolver.resolveRedirect(authRequest.getRedirectUri(), client); + + try { + URIBuilder uriBuilder = new URIBuilder(url); + + uriBuilder.addParameter(ERROR, LOGIN_REQUIRED); + if (!Strings.isNullOrEmpty(authRequest.getState())) { + uriBuilder.addParameter(STATE, authRequest.getState()); // copy the state parameter if one was given + } + + response.sendRedirect(uriBuilder.toString()); + return; + + } catch (URISyntaxException e) { + logger.error("Can't build redirect URI for prompt=none, sending error instead", e); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); + return; + } + } + + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); + return; + } + } else if (prompts.contains(PROMPT_LOGIN)) { + + // first see if the user's already been prompted in this session + if (session.getAttribute(PROMPTED) == null) { + // user hasn't been PROMPTED yet, we need to check + + session.setAttribute(PROMPT_REQUESTED, Boolean.TRUE); + + // see if the user's logged in + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + // user's been logged in already (by session management) + // log them out and continue + SecurityContextHolder.getContext().setAuthentication(null); + chain.doFilter(req, res); + } else { + // user hasn't been logged in yet, we can keep going since we'll get there + chain.doFilter(req, res); + } + } else { + // user has been PROMPTED, we're fine + + // but first, undo the prompt tag + session.removeAttribute(PROMPTED); + chain.doFilter(req, res); + } + } else { + // prompt parameter is a value we don't care about, not our business + chain.doFilter(req, res); + } + + } else if (authRequest.getExtensions().get(MAX_AGE) != null || + (client != null && client.getDefaultMaxAge() != null)) { + + // default to the client's stored value, check the string parameter + Integer max = (client != null ? client.getDefaultMaxAge() : null); + String maxAge = (String) authRequest.getExtensions().get(MAX_AGE); + if (maxAge != null) { + max = Integer.parseInt(maxAge); + } + + if (max != null) { + + Date authTime = (Date) session.getAttribute(AuthenticationTimeStamper.AUTH_TIMESTAMP); + + Date now = new Date(); + if (authTime != null) { + long seconds = (now.getTime() - authTime.getTime()) / 1000; + if (seconds > max) { + // session is too old, log the user out and continue + SecurityContextHolder.getContext().setAuthentication(null); + } + } + } + chain.doFilter(req, res); + } else { + // no prompt parameter, not our business + chain.doFilter(req, res); + } + + } catch (InvalidClientException e) { + // we couldn't find the client, move on and let the rest of the system catch the error + chain.doFilter(req, res); + } + } + + /** + * @param parameterMap + * @return + */ + private Map createRequestMap(Map parameterMap) { + Map requestMap = new HashMap<>(); + for (String key : parameterMap.keySet()) { + String[] val = parameterMap.get(key); + if (val != null && val.length > 0) { + requestMap.put(key, val[0]); // add the first value only (which is what Spring seems to do) + } + } + + return requestMap; + } + + /** + * @return the requestMatcher + */ + public RequestMatcher getRequestMatcher() { + return requestMatcher; + } + + /** + * @param requestMatcher the requestMatcher to set + */ + public void setRequestMatcher(RequestMatcher requestMatcher) { + this.requestMatcher = requestMatcher; + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/MultiUrlRequestMatcher.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/MultiUrlRequestMatcher.java new file mode 100644 index 0000000000..c2c140b560 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/MultiUrlRequestMatcher.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.filter; + +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.web.util.UrlUtils; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * @author jricher + * + */ +public class MultiUrlRequestMatcher implements RequestMatcher { + private final Set matchers; + + public MultiUrlRequestMatcher(Set filterProcessesUrls) { + this.matchers = new HashSet<>(filterProcessesUrls.size()); + for (String filterProcessesUrl : filterProcessesUrls) { + Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified"); + Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid URL"); + matchers.add(new AntPathRequestMatcher(filterProcessesUrl)); + } + + } + + @Override + public boolean matches(HttpServletRequest request) { + for (RequestMatcher matcher : matchers) { + if (matcher.matches(request)) { + return true; + } + } + + return false; + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/PromptFilter.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/PromptFilter.java deleted file mode 100644 index 23da6f8aa7..0000000000 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/PromptFilter.java +++ /dev/null @@ -1,199 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.openid.connect.filter; - -import java.io.IOException; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import org.mitre.oauth2.model.ClientDetailsEntity; -import org.mitre.oauth2.service.ClientDetailsEntityService; -import org.mitre.openid.connect.web.AuthenticationTimeStamper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.common.exceptions.InvalidClientException; -import org.springframework.security.oauth2.provider.AuthorizationRequest; -import org.springframework.security.oauth2.provider.OAuth2RequestFactory; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.GenericFilterBean; - -import com.google.common.base.Splitter; -import com.google.common.base.Strings; - -/** - * @author jricher - * - */ -@Component("promptFilter") -public class PromptFilter extends GenericFilterBean { - - private Logger logger = LoggerFactory.getLogger(PromptFilter.class); - - public final static String PROMPTED = "PROMPT_FILTER_PROMPTED"; - public final static String PROMPT_REQUESTED = "PROMPT_FILTER_REQUESTED"; - - @Autowired - private OAuth2RequestFactory authRequestFactory; - - @Autowired - private ClientDetailsEntityService clientService; - - /** - * - */ - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - - // skip everything that's not an authorize URL - if (!request.getServletPath().startsWith("/authorize")) { - chain.doFilter(req, res); - return; - } - - // we have to create our own auth request in order to get at all the parmeters appropriately - AuthorizationRequest authRequest = authRequestFactory.createAuthorizationRequest(createRequestMap(request.getParameterMap())); - - ClientDetailsEntity client = null; - - try { - client = clientService.loadClientByClientId(authRequest.getClientId()); - } catch (InvalidClientException e) { - // no need to worry about this here, it would be caught elsewhere - } catch (IllegalArgumentException e) { - // no need to worry about this here, it would be caught elsewhere - } - - if (authRequest.getExtensions().get("prompt") != null) { - // we have a "prompt" parameter - String prompt = (String)authRequest.getExtensions().get("prompt"); - List prompts = Splitter.on(" ").splitToList(Strings.nullToEmpty(prompt)); - - if (prompts.contains("none")) { - logger.info("Client requested no prompt"); - // see if the user's logged in - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - if (auth != null) { - // user's been logged in already (by session management) - // we're OK, continue without prompting - chain.doFilter(req, res); - } else { - // user hasn't been logged in, we need to "return an error" - logger.info("User not logged in, no prompt requested, returning 403 from filter"); - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); - return; - } - } else if (prompts.contains("login")) { - - // first see if the user's already been prompted in this session - HttpSession session = request.getSession(); - if (session.getAttribute(PROMPTED) == null) { - // user hasn't been PROMPTED yet, we need to check - - session.setAttribute(PROMPT_REQUESTED, Boolean.TRUE); - - // see if the user's logged in - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - // user's been logged in already (by session management) - // log them out and continue - SecurityContextHolder.getContext().setAuthentication(null); - chain.doFilter(req, res); - } else { - // user hasn't been logged in yet, we can keep going since we'll get there - chain.doFilter(req, res); - } - } else { - // user has been PROMPTED, we're fine - - // but first, undo the prompt tag - session.removeAttribute(PROMPTED); - chain.doFilter(req, res); - } - } else { - // prompt parameter is a value we don't care about, not our business - chain.doFilter(req, res); - } - - } else if (authRequest.getExtensions().get("max_age") != null || - (client != null && client.getDefaultMaxAge() != null)) { - - // default to the client's stored value, check the string parameter - Integer max = (client != null ? client.getDefaultMaxAge() : null); - String maxAge = (String) authRequest.getExtensions().get("max_age"); - if (maxAge != null) { - max = Integer.parseInt(maxAge); - } - - if (max != null) { - - HttpSession session = request.getSession(); - Date authTime = (Date) session.getAttribute(AuthenticationTimeStamper.AUTH_TIMESTAMP); - - Date now = new Date(); - if (authTime != null) { - long seconds = (now.getTime() - authTime.getTime()) / 1000; - if (seconds > max) { - // session is too old, log the user out and continue - SecurityContextHolder.getContext().setAuthentication(null); - } - } - } - chain.doFilter(req, res); - } else { - // no prompt parameter, not our business - chain.doFilter(req, res); - } - - } - - /** - * @param parameterMap - * @return - */ - private Map createRequestMap(Map parameterMap) { - Map requestMap = new HashMap(); - for (String key : parameterMap.keySet()) { - String[] val = parameterMap.get(key); - if (val != null && val.length > 0) { - requestMap.put(key, val[0]); // add the first value only (which is what Spring seems to do) - } - } - - return requestMap; - } - -} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaAddressRepository.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaAddressRepository.java index ff8ba94e0f..c2556d62f1 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaAddressRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaAddressRepository.java @@ -1,23 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository.impl; -import static org.mitre.util.jpa.JpaUtil.saveOrUpdate; - import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @@ -28,45 +27,20 @@ /** * JPA Address repository implementation - * + * * @author Michael Joseph Walsh - * + * */ @Repository public class JpaAddressRepository implements AddressRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Address getById(Long id) { return manager.find(Address.class, id); } - @Override - @Transactional - public void remove(Address address) { - Address found = manager.find(Address.class, address.getId()); - - if (found != null) { - manager.remove(address); - } else { - throw new IllegalArgumentException(); - } - } - - @Override - @Transactional - public void removeById(Long id) { - Address found = getById(id); - - manager.remove(found); - } - - @Override - @Transactional - public Address save(Address address) { - return saveOrUpdate(address.getId(), manager, address); - } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaApprovedSiteRepository.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaApprovedSiteRepository.java index 12498ffce4..de30df5130 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaApprovedSiteRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaApprovedSiteRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository.impl; import static org.mitre.util.jpa.JpaUtil.saveOrUpdate; @@ -31,32 +32,31 @@ /** * JPA ApprovedSite repository implementation - * + * * @author Michael Joseph Walsh, aanganes * */ @Repository public class JpaApprovedSiteRepository implements ApprovedSiteRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Collection getAll() { - TypedQuery query = manager.createNamedQuery( - "ApprovedSite.getAll", ApprovedSite.class); + TypedQuery query = manager.createNamedQuery(ApprovedSite.QUERY_ALL, ApprovedSite.class); return query.getResultList(); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public ApprovedSite getById(Long id) { return manager.find(ApprovedSite.class, id); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void remove(ApprovedSite approvedSite) { ApprovedSite found = manager.find(ApprovedSite.class, approvedSite.getId()); @@ -68,7 +68,7 @@ public void remove(ApprovedSite approvedSite) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public ApprovedSite save(ApprovedSite approvedSite) { return saveOrUpdate(approvedSite.getId(), manager, approvedSite); } @@ -76,28 +76,28 @@ public ApprovedSite save(ApprovedSite approvedSite) { @Override public Collection getByClientIdAndUserId(String clientId, String userId) { - TypedQuery query = manager.createNamedQuery("ApprovedSite.getByClientIdAndUserId", ApprovedSite.class); - query.setParameter("userId", userId); - query.setParameter("clientId", clientId); + TypedQuery query = manager.createNamedQuery(ApprovedSite.QUERY_BY_CLIENT_ID_AND_USER_ID, ApprovedSite.class); + query.setParameter(ApprovedSite.PARAM_USER_ID, userId); + query.setParameter(ApprovedSite.PARAM_CLIENT_ID, clientId); return query.getResultList(); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Collection getByUserId(String userId) { - TypedQuery query = manager.createNamedQuery("ApprovedSite.getByUserId", ApprovedSite.class); - query.setParameter("userId", userId); + TypedQuery query = manager.createNamedQuery(ApprovedSite.QUERY_BY_USER_ID, ApprovedSite.class); + query.setParameter(ApprovedSite.PARAM_USER_ID, userId); return query.getResultList(); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Collection getByClientId(String clientId) { - TypedQuery query = manager.createNamedQuery("ApprovedSite.getByClientId", ApprovedSite.class); - query.setParameter("clientId", clientId); + TypedQuery query = manager.createNamedQuery(ApprovedSite.QUERY_BY_CLIENT_ID, ApprovedSite.class); + query.setParameter(ApprovedSite.PARAM_CLIENT_ID, clientId); return query.getResultList(); } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaBlacklistedSiteRepository.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaBlacklistedSiteRepository.java index 3ad17a9c10..b56a137c42 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaBlacklistedSiteRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaBlacklistedSiteRepository.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository.impl; @@ -39,16 +40,16 @@ @Repository public class JpaBlacklistedSiteRepository implements BlacklistedSiteRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; /* (non-Javadoc) * @see org.mitre.openid.connect.repository.BlacklistedSiteRepository#getAll() */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Collection getAll() { - TypedQuery query = manager.createNamedQuery("BlacklistedSite.getAll", BlacklistedSite.class); + TypedQuery query = manager.createNamedQuery(BlacklistedSite.QUERY_ALL, BlacklistedSite.class); return query.getResultList(); } @@ -56,7 +57,7 @@ public Collection getAll() { * @see org.mitre.openid.connect.repository.BlacklistedSiteRepository#getById(java.lang.Long) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public BlacklistedSite getById(Long id) { return manager.find(BlacklistedSite.class, id); } @@ -65,7 +66,7 @@ public BlacklistedSite getById(Long id) { * @see org.mitre.openid.connect.repository.BlacklistedSiteRepository#remove(org.mitre.openid.connect.model.BlacklistedSite) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void remove(BlacklistedSite blacklistedSite) { BlacklistedSite found = manager.find(BlacklistedSite.class, blacklistedSite.getId()); @@ -81,7 +82,7 @@ public void remove(BlacklistedSite blacklistedSite) { * @see org.mitre.openid.connect.repository.BlacklistedSiteRepository#save(org.mitre.openid.connect.model.BlacklistedSite) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public BlacklistedSite save(BlacklistedSite blacklistedSite) { return saveOrUpdate(blacklistedSite.getId(), manager, blacklistedSite); } @@ -90,7 +91,7 @@ public BlacklistedSite save(BlacklistedSite blacklistedSite) { * @see org.mitre.openid.connect.repository.BlacklistedSiteRepository#update(org.mitre.openid.connect.model.BlacklistedSite, org.mitre.openid.connect.model.BlacklistedSite) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public BlacklistedSite update(BlacklistedSite oldBlacklistedSite, BlacklistedSite blacklistedSite) { blacklistedSite.setId(oldBlacklistedSite.getId()); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaPairwiseIdentifierRepository.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaPairwiseIdentifierRepository.java index 0ed38f9fdb..a92acf3bd6 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaPairwiseIdentifierRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaPairwiseIdentifierRepository.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.repository.impl; @@ -38,7 +39,7 @@ @Repository public class JpaPairwiseIdentifierRepository implements PairwiseIdentifierRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; /* (non-Javadoc) @@ -46,9 +47,9 @@ public class JpaPairwiseIdentifierRepository implements PairwiseIdentifierReposi */ @Override public PairwiseIdentifier getBySectorIdentifier(String sub, String sectorIdentifierUri) { - TypedQuery query = manager.createNamedQuery("PairwiseIdentifier.getBySectorIdentifier", PairwiseIdentifier.class); - query.setParameter("sub", sub); - query.setParameter("sectorIdentifier", sectorIdentifierUri); + TypedQuery query = manager.createNamedQuery(PairwiseIdentifier.QUERY_BY_SECTOR_IDENTIFIER, PairwiseIdentifier.class); + query.setParameter(PairwiseIdentifier.PARAM_SUB, sub); + query.setParameter(PairwiseIdentifier.PARAM_SECTOR_IDENTIFIER, sectorIdentifierUri); return getSingleResult(query.getResultList()); } @@ -57,7 +58,7 @@ public PairwiseIdentifier getBySectorIdentifier(String sub, String sectorIdentif * @see org.mitre.openid.connect.repository.PairwiseIdentifierRepository#save(org.mitre.openid.connect.model.PairwiseIdentifier) */ @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void save(PairwiseIdentifier pairwise) { saveOrUpdate(pairwise.getId(), manager, pairwise); } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaUserInfoRepository.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaUserInfoRepository.java index 88651eb0e1..7627246da3 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaUserInfoRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaUserInfoRepository.java @@ -1,25 +1,23 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository.impl; import static org.mitre.util.jpa.JpaUtil.getSingleResult; -import static org.mitre.util.jpa.JpaUtil.saveOrUpdate; - -import java.util.Collection; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @@ -29,68 +27,40 @@ import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.repository.UserInfoRepository; import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; /** * JPA UserInfo repository implementation - * + * * @author Michael Joseph Walsh * */ -@Repository +@Repository("jpaUserInfoRepository") public class JpaUserInfoRepository implements UserInfoRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; + /** + * Get a single UserInfo object by its username + */ @Override - @Transactional - public UserInfo getBySubject(String sub) { - TypedQuery query = manager.createNamedQuery("DefaultUserInfo.getBySubject", DefaultUserInfo.class); - query.setParameter("sub", sub); + public UserInfo getByUsername(String username) { + TypedQuery query = manager.createNamedQuery(DefaultUserInfo.QUERY_BY_USERNAME, DefaultUserInfo.class); + query.setParameter(DefaultUserInfo.PARAM_USERNAME, username); return getSingleResult(query.getResultList()); - } - - @Override - @Transactional - public UserInfo save(UserInfo userInfo) { - DefaultUserInfo dui = (DefaultUserInfo)userInfo; - return saveOrUpdate(dui.getId(), manager, dui); - } - - @Override - @Transactional - public void remove(UserInfo userInfo) { - DefaultUserInfo dui = (DefaultUserInfo)userInfo; - UserInfo found = manager.find(DefaultUserInfo.class, dui.getId()); - - if (found != null) { - manager.remove(userInfo); - } else { - throw new IllegalArgumentException(); - } - } - @Override - @Transactional - public Collection getAll() { - - TypedQuery query = manager.createNamedQuery("DefaultUserInfo.getAll", DefaultUserInfo.class); - - return query.getResultList(); } /** - * Get a single UserInfo object by its username + * Get a single UserInfo object by its email address */ @Override - public UserInfo getByUsername(String username) { - TypedQuery query = manager.createNamedQuery("DefaultUserInfo.getByUsername", DefaultUserInfo.class); - query.setParameter("username", username); + public UserInfo getByEmailAddress(String email) { + TypedQuery query = manager.createNamedQuery(DefaultUserInfo.QUERY_BY_EMAIL, DefaultUserInfo.class); + query.setParameter(DefaultUserInfo.PARAM_EMAIL, email); return getSingleResult(query.getResultList()); - } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaWhitelistedSiteRepository.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaWhitelistedSiteRepository.java index 41cdb4c00c..98173588b7 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaWhitelistedSiteRepository.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/repository/impl/JpaWhitelistedSiteRepository.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.repository.impl; import static org.mitre.util.jpa.JpaUtil.saveOrUpdate; @@ -32,31 +33,31 @@ /** * JPA WhitelistedSite repository implementation - * + * * @author Michael Joseph Walsh, aanganes * */ @Repository public class JpaWhitelistedSiteRepository implements WhitelistedSiteRepository { - @PersistenceContext + @PersistenceContext(unitName="defaultPersistenceUnit") private EntityManager manager; @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Collection getAll() { - TypedQuery query = manager.createNamedQuery("WhitelistedSite.getAll", WhitelistedSite.class); + TypedQuery query = manager.createNamedQuery(WhitelistedSite.QUERY_ALL, WhitelistedSite.class); return query.getResultList(); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public WhitelistedSite getById(Long id) { return manager.find(WhitelistedSite.class, id); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void remove(WhitelistedSite whitelistedSite) { WhitelistedSite found = manager.find(WhitelistedSite.class, whitelistedSite.getId()); @@ -68,13 +69,13 @@ public void remove(WhitelistedSite whitelistedSite) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public WhitelistedSite save(WhitelistedSite whiteListedSite) { return saveOrUpdate(whiteListedSite.getId(), manager, whiteListedSite); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public WhitelistedSite update(WhitelistedSite oldWhitelistedSite, WhitelistedSite whitelistedSite) { // sanity check whitelistedSite.setId(oldWhitelistedSite.getId()); @@ -83,18 +84,18 @@ public WhitelistedSite update(WhitelistedSite oldWhitelistedSite, WhitelistedSit } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public WhitelistedSite getByClientId(String clientId) { - TypedQuery query = manager.createNamedQuery("WhitelistedSite.getByClientId", WhitelistedSite.class); - query.setParameter("clientId", clientId); + TypedQuery query = manager.createNamedQuery(WhitelistedSite.QUERY_BY_CLIENT_ID, WhitelistedSite.class); + query.setParameter(WhitelistedSite.PARAM_CLIENT_ID, clientId); return JpaUtil.getSingleResult(query.getResultList()); } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public Collection getByCreator(String creatorId) { - TypedQuery query = manager.createNamedQuery("WhitelistedSite.getByCreaterUserId", WhitelistedSite.class); - query.setParameter("userId", creatorId); + TypedQuery query = manager.createNamedQuery(WhitelistedSite.QUERY_BY_CREATOR, WhitelistedSite.class); + query.setParameter(WhitelistedSite.PARAM_USER_ID, creatorId); return query.getResultList(); } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/ConnectOAuth2RequestFactory.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectOAuth2RequestFactory.java similarity index 63% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/ConnectOAuth2RequestFactory.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectOAuth2RequestFactory.java index 37f68798e3..d957f31a4f 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/ConnectOAuth2RequestFactory.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectOAuth2RequestFactory.java @@ -1,34 +1,51 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect; - + *******************************************************************************/ +package org.mitre.openid.connect.request; + + +import static org.mitre.openid.connect.request.ConnectRequestParameters.AUD; +import static org.mitre.openid.connect.request.ConnectRequestParameters.CLAIMS; +import static org.mitre.openid.connect.request.ConnectRequestParameters.CLIENT_ID; +import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE_METHOD; +import static org.mitre.openid.connect.request.ConnectRequestParameters.DISPLAY; +import static org.mitre.openid.connect.request.ConnectRequestParameters.LOGIN_HINT; +import static org.mitre.openid.connect.request.ConnectRequestParameters.MAX_AGE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.NONCE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT; +import static org.mitre.openid.connect.request.ConnectRequestParameters.REDIRECT_URI; +import static org.mitre.openid.connect.request.ConnectRequestParameters.REQUEST; +import static org.mitre.openid.connect.request.ConnectRequestParameters.RESPONSE_TYPE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.SCOPE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.STATE; + +import java.io.Serializable; import java.text.ParseException; import java.util.Collections; import java.util.Map; import java.util.Set; -import java.util.UUID; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; -import org.mitre.jwt.signer.service.impl.JWKSetCacheService; -import org.mitre.jwt.signer.service.impl.SymmetricCacheService; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.ClientKeyCacheService; import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.PKCEAlgorithm; import org.mitre.oauth2.service.ClientDetailsEntityService; -import org.mitre.oauth2.service.SystemScopeService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -48,37 +65,33 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jwt.EncryptedJWT; import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.PlainJWT; -import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; import com.nimbusds.jwt.SignedJWT; @Component("connectOAuth2RequestFactory") public class ConnectOAuth2RequestFactory extends DefaultOAuth2RequestFactory { - private static Logger logger = LoggerFactory.getLogger(ConnectOAuth2RequestFactory.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ConnectOAuth2RequestFactory.class); private ClientDetailsEntityService clientDetailsService; @Autowired - private JWKSetCacheService validators; - - @Autowired - private SymmetricCacheService symmetricCacheService; + private ClientKeyCacheService validators; @Autowired - private SystemScopeService systemScopes; - - @Autowired - private JwtEncryptionAndDecryptionService encryptionService; + private JWTEncryptionAndDecryptionService encryptionService; private JsonParser parser = new JsonParser(); /** * Constructor with arguments - * + * * @param clientDetailsService - * @param nonceService */ @Autowired public ConnectOAuth2RequestFactory(ClientDetailsEntityService clientDetailsService) { @@ -99,27 +112,46 @@ public AuthorizationRequest createAuthorizationRequest(Map input //Add extension parameters to the 'extensions' map - if (inputParams.containsKey("prompt")) { - request.getExtensions().put("prompt", inputParams.get("prompt")); + if (inputParams.containsKey(PROMPT)) { + request.getExtensions().put(PROMPT, inputParams.get(PROMPT)); } - if (inputParams.containsKey("nonce")) { - request.getExtensions().put("nonce", inputParams.get("nonce")); + if (inputParams.containsKey(NONCE)) { + request.getExtensions().put(NONCE, inputParams.get(NONCE)); } - if (inputParams.containsKey("claims")) { - JsonObject claimsRequest = parseClaimRequest(inputParams.get("claims")); + if (inputParams.containsKey(CLAIMS)) { + JsonObject claimsRequest = parseClaimRequest(inputParams.get(CLAIMS)); if (claimsRequest != null) { - request.getExtensions().put("claims", claimsRequest.toString()); + request.getExtensions().put(CLAIMS, claimsRequest.toString()); } } - if (inputParams.containsKey("max_age")) { - request.getExtensions().put("max_age", inputParams.get("max_age")); + if (inputParams.containsKey(MAX_AGE)) { + request.getExtensions().put(MAX_AGE, inputParams.get(MAX_AGE)); + } + + if (inputParams.containsKey(LOGIN_HINT)) { + request.getExtensions().put(LOGIN_HINT, inputParams.get(LOGIN_HINT)); } - if (inputParams.containsKey("request")) { - request.getExtensions().put("request", inputParams.get("request")); - processRequestObject(inputParams.get("request"), request); + if (inputParams.containsKey(AUD)) { + request.getExtensions().put(AUD, inputParams.get(AUD)); + } + + if (inputParams.containsKey(CODE_CHALLENGE)) { + request.getExtensions().put(CODE_CHALLENGE, inputParams.get(CODE_CHALLENGE)); + if (inputParams.containsKey(CODE_CHALLENGE_METHOD)) { + request.getExtensions().put(CODE_CHALLENGE_METHOD, inputParams.get(CODE_CHALLENGE_METHOD)); + } else { + // if the client doesn't specify a code challenge transformation method, it's "plain" + request.getExtensions().put(CODE_CHALLENGE_METHOD, PKCEAlgorithm.plain.getName()); + } + + } + + if (inputParams.containsKey(REQUEST)) { + request.getExtensions().put(REQUEST, inputParams.get(REQUEST)); + processRequestObject(inputParams.get(REQUEST), request); } if (request.getClientId() != null) { @@ -131,27 +163,21 @@ public AuthorizationRequest createAuthorizationRequest(Map input request.setScope(clientScopes); } - if (request.getExtensions().get("max_age") == null && client.getDefaultMaxAge() != null) { - request.getExtensions().put("max_age", client.getDefaultMaxAge().toString()); + if (request.getExtensions().get(MAX_AGE) == null && client.getDefaultMaxAge() != null) { + request.getExtensions().put(MAX_AGE, client.getDefaultMaxAge().toString()); } } catch (OAuth2Exception e) { logger.error("Caught OAuth2 exception trying to test client scopes and max age:", e); } } - - // add CSRF protection to the request on first parse - String csrf = UUID.randomUUID().toString(); - request.getExtensions().put("csrf", csrf); - - - return request; } /** - * @param inputParams - * @return + * + * @param jwtString + * @param request */ private void processRequestObject(String jwtString, AuthorizationRequest request) { @@ -159,8 +185,6 @@ private void processRequestObject(String jwtString, AuthorizationRequest request try { JWT jwt = JWTParser.parse(jwtString); - // TODO: move keys to constants - if (jwt instanceof SignedJWT) { // it's a signed JWT, check the signature @@ -168,7 +192,7 @@ private void processRequestObject(String jwtString, AuthorizationRequest request // need to check clientId first so that we can load the client to check other fields if (request.getClientId() == null) { - request.setClientId(signedJwt.getJWTClaimsSet().getStringClaim("client_id")); + request.setClientId(signedJwt.getJWTClaimsSet().getStringClaim(CLIENT_ID)); } ClientDetailsEntity client = clientDetailsService.loadClientByClientId(request.getClientId()); @@ -185,52 +209,22 @@ private void processRequestObject(String jwtString, AuthorizationRequest request throw new InvalidClientException("Client's registered request object signing algorithm (" + client.getRequestObjectSigningAlg() + ") does not match request object's actual algorithm (" + alg.getName() + ")"); } - if (alg.equals(JWSAlgorithm.RS256) - || alg.equals(JWSAlgorithm.RS384) - || alg.equals(JWSAlgorithm.RS512)) { - - // it's RSA, need to find the JWK URI and fetch the key - - if (client.getJwksUri() == null) { - throw new InvalidClientException("Client must have a JWKS URI registered to use signed request objects."); - } - - // check JWT signature - JwtSigningAndValidationService validator = validators.getValidator(client.getJwksUri()); - - if (validator == null) { - throw new InvalidClientException("Unable to create signature validator for client's JWKS URI: " + client.getJwksUri()); - } - - if (!validator.validateSignature(signedJwt)) { - throw new InvalidClientException("Signature did not validate for presented JWT request object."); - } - } else if (alg.equals(JWSAlgorithm.HS256) - || alg.equals(JWSAlgorithm.HS384) - || alg.equals(JWSAlgorithm.HS512)) { - - // it's HMAC, we need to make a validator based on the client secret - - JwtSigningAndValidationService validator = symmetricCacheService.getSymmetricValidtor(client); - - if (validator == null) { - throw new InvalidClientException("Unable to create signature validator for client's secret: " + client.getClientSecret()); - } - - if (!validator.validateSignature(signedJwt)) { - throw new InvalidClientException("Signature did not validate for presented JWT request object."); - } - + JWTSigningAndValidationService validator = validators.getValidator(client, alg); + if (validator == null) { + throw new InvalidClientException("Unable to create signature validator for client " + client + " and algorithm " + alg); } + if (!validator.validateSignature(signedJwt)) { + throw new InvalidClientException("Signature did not validate for presented JWT request object."); + } } else if (jwt instanceof PlainJWT) { PlainJWT plainJwt = (PlainJWT)jwt; // need to check clientId first so that we can load the client to check other fields if (request.getClientId() == null) { - request.setClientId(plainJwt.getJWTClaimsSet().getStringClaim("client_id")); + request.setClientId(plainJwt.getJWTClaimsSet().getStringClaim(CLIENT_ID)); } ClientDetailsEntity client = clientDetailsService.loadClientByClientId(request.getClientId()); @@ -263,7 +257,7 @@ private void processRequestObject(String jwtString, AuthorizationRequest request // need to check clientId first so that we can load the client to check other fields if (request.getClientId() == null) { - request.setClientId(encryptedJWT.getJWTClaimsSet().getStringClaim("client_id")); + request.setClientId(encryptedJWT.getJWTClaimsSet().getStringClaim(CLIENT_ID)); } ClientDetailsEntity client = clientDetailsService.loadClientByClientId(request.getClientId()); @@ -282,17 +276,17 @@ private void processRequestObject(String jwtString, AuthorizationRequest request // now that we've got the JWT, and it's been parsed, validated, and/or decrypted, we can process the claims - ReadOnlyJWTClaimsSet claims = jwt.getJWTClaimsSet(); + JWTClaimsSet claims = jwt.getJWTClaimsSet(); - Set responseTypes = OAuth2Utils.parseParameterList(claims.getStringClaim("response_type")); - if (responseTypes != null && !responseTypes.isEmpty()) { + Set responseTypes = OAuth2Utils.parseParameterList(claims.getStringClaim(RESPONSE_TYPE)); + if (!responseTypes.isEmpty()) { if (!responseTypes.equals(request.getResponseTypes())) { logger.info("Mismatch between request object and regular parameter for response_type, using request object"); } request.setResponseTypes(responseTypes); } - String redirectUri = claims.getStringClaim("redirect_uri"); + String redirectUri = claims.getStringClaim(REDIRECT_URI); if (redirectUri != null) { if (!redirectUri.equals(request.getRedirectUri())) { logger.info("Mismatch between request object and regular parameter for redirect_uri, using request object"); @@ -300,7 +294,7 @@ private void processRequestObject(String jwtString, AuthorizationRequest request request.setRedirectUri(redirectUri); } - String state = claims.getStringClaim("state"); + String state = claims.getStringClaim(STATE); if(state != null) { if (!state.equals(request.getState())) { logger.info("Mismatch between request object and regular parameter for state, using request object"); @@ -308,45 +302,54 @@ private void processRequestObject(String jwtString, AuthorizationRequest request request.setState(state); } - String nonce = claims.getStringClaim("nonce"); + String nonce = claims.getStringClaim(NONCE); if(nonce != null) { - if (!nonce.equals(request.getExtensions().get("nonce"))) { + if (!nonce.equals(request.getExtensions().get(NONCE))) { logger.info("Mismatch between request object and regular parameter for nonce, using request object"); } - request.getExtensions().put("nonce", nonce); + request.getExtensions().put(NONCE, nonce); } - String display = claims.getStringClaim("display"); + String display = claims.getStringClaim(DISPLAY); if (display != null) { - if (!display.equals(request.getExtensions().get("display"))) { + if (!display.equals(request.getExtensions().get(DISPLAY))) { logger.info("Mismatch between request object and regular parameter for display, using request object"); } - request.getExtensions().put("display", display); + request.getExtensions().put(DISPLAY, display); } - String prompt = claims.getStringClaim("prompt"); + String prompt = claims.getStringClaim(PROMPT); if (prompt != null) { - if (!prompt.equals(request.getExtensions().get("prompt"))) { + if (!prompt.equals(request.getExtensions().get(PROMPT))) { logger.info("Mismatch between request object and regular parameter for prompt, using request object"); } - request.getExtensions().put("prompt", prompt); + request.getExtensions().put(PROMPT, prompt); } - Set scope = OAuth2Utils.parseParameterList(claims.getStringClaim("scope")); - if (scope != null && !scope.isEmpty()) { + Set scope = OAuth2Utils.parseParameterList(claims.getStringClaim(SCOPE)); + if (!scope.isEmpty()) { if (!scope.equals(request.getScope())) { logger.info("Mismatch between request object and regular parameter for scope, using request object"); } request.setScope(scope); } - JsonObject claimRequest = parseClaimRequest(claims.getStringClaim("claims")); + JsonObject claimRequest = parseClaimRequest(claims.getStringClaim(CLAIMS)); if (claimRequest != null) { - if (!claimRequest.equals(parseClaimRequest(request.getExtensions().get("claims").toString()))) { + Serializable claimExtension = request.getExtensions().get(CLAIMS); + if (claimExtension == null || !claimRequest.equals(parseClaimRequest(claimExtension.toString()))) { logger.info("Mismatch between request object and regular parameter for claims, using request object"); } // we save the string because the object might not be a Java Serializable, and we can parse it easily enough anyway - request.getExtensions().put("claims", claimRequest.toString()); + request.getExtensions().put(CLAIMS, claimRequest.toString()); + } + + String loginHint = claims.getStringClaim(LOGIN_HINT); + if (loginHint != null) { + if (!loginHint.equals(request.getExtensions().get(LOGIN_HINT))) { + logger.info("Mistmatch between request object and regular parameter for login_hint, using requst object"); + } + request.getExtensions().put(LOGIN_HINT, loginHint); } } catch (ParseException e) { diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectRequestParameters.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectRequestParameters.java new file mode 100644 index 0000000000..cebcdd169f --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectRequestParameters.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.request; + +public interface ConnectRequestParameters { + + public String CLIENT_ID = "client_id"; + public String RESPONSE_TYPE = "response_type"; + public String REDIRECT_URI = "redirect_uri"; + public String STATE = "state"; + public String DISPLAY = "display"; + public String REQUEST = "request"; + public String LOGIN_HINT = "login_hint"; + public String MAX_AGE = "max_age"; + public String CLAIMS = "claims"; + public String SCOPE = "scope"; + public String NONCE = "nonce"; + public String PROMPT = "prompt"; + + // prompt values + public String PROMPT_LOGIN = "login"; + public String PROMPT_NONE = "none"; + public String PROMPT_CONSENT = "consent"; + public String PROMPT_SEPARATOR = " "; + + // extensions + public String APPROVED_SITE = "approved_site"; + + // responses + public String ERROR = "error"; + public String LOGIN_REQUIRED = "login_required"; + + // audience + public String AUD = "aud"; + + // PKCE + public String CODE_CHALLENGE = "code_challenge"; + public String CODE_CHALLENGE_METHOD = "code_challenge_method"; + public String CODE_VERIFIER = "code_verifier"; + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultApprovedSiteService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultApprovedSiteService.java index e3825ef57a..05ddd5c0c3 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultApprovedSiteService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultApprovedSiteService.java @@ -1,29 +1,30 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.Set; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.repository.OAuth2TokenRepository; import org.mitre.openid.connect.model.ApprovedSite; -import org.mitre.openid.connect.model.WhitelistedSite; import org.mitre.openid.connect.repository.ApprovedSiteRepository; import org.mitre.openid.connect.service.ApprovedSiteService; import org.mitre.openid.connect.service.StatsService; @@ -39,14 +40,17 @@ /** * Implementation of the ApprovedSiteService - * + * * @author Michael Joseph Walsh, aanganes * */ @Service("defaultApprovedSiteService") public class DefaultApprovedSiteService implements ApprovedSiteService { - private static Logger logger = LoggerFactory.getLogger(DefaultApprovedSiteService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultApprovedSiteService.class); @Autowired private ApprovedSiteRepository approvedSiteRepository; @@ -63,7 +67,7 @@ public Collection getAll() { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public ApprovedSite save(ApprovedSite approvedSite) { ApprovedSite a = approvedSiteRepository.save(approvedSite); statsService.resetCache(); @@ -76,11 +80,11 @@ public ApprovedSite getById(Long id) { } @Override - @Transactional + @Transactional(value="defaultTransactionManager") public void remove(ApprovedSite approvedSite) { //Remove any associated access and refresh tokens - Set accessTokens = approvedSite.getApprovedAccessTokens(); + List accessTokens = getApprovedAccessTokens(approvedSite); for (OAuth2AccessTokenEntity token : accessTokens) { if (token.getRefreshToken() != null) { @@ -95,9 +99,8 @@ public void remove(ApprovedSite approvedSite) { } @Override - @Transactional - public ApprovedSite createApprovedSite(String clientId, String userId, Date timeoutDate, Set allowedScopes, - WhitelistedSite whitelistedSite) { + @Transactional(value="defaultTransactionManager") + public ApprovedSite createApprovedSite(String clientId, String userId, Date timeoutDate, Set allowedScopes) { ApprovedSite as = approvedSiteRepository.save(new ApprovedSite()); @@ -108,7 +111,6 @@ public ApprovedSite createApprovedSite(String clientId, String userId, Date time as.setUserId(userId); as.setTimeoutDate(timeoutDate); as.setAllowedScopes(allowedScopes); - as.setWhitelistedSite(whitelistedSite); return save(as); @@ -155,10 +157,12 @@ public void clearApprovedSitesForClient(ClientDetails client) { @Override public void clearExpiredSites() { - logger.info("Clearing expired approved sites"); + logger.debug("Clearing expired approved sites"); Collection expiredSites = getExpired(); - logger.info("Found " + expiredSites.size() + " expired approved sites."); + if (expiredSites.size() > 0) { + logger.info("Found " + expiredSites.size() + " expired approved sites."); + } if (expiredSites != null) { for (ApprovedSite expired : expiredSites) { remove(expired); @@ -178,4 +182,11 @@ private Collection getExpired() { return Collections2.filter(approvedSiteRepository.getAll(), isExpired); } + @Override + public List getApprovedAccessTokens( + ApprovedSite approvedSite) { + return tokenRepository.getAccessTokensForApprovedSite(approvedSite); + + } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultBlacklistedSiteService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultBlacklistedSiteService.java index 3cd03f5be7..65652b6dc7 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultBlacklistedSiteService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultBlacklistedSiteService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; @@ -35,7 +36,7 @@ * */ @Service -@Transactional +@Transactional(value="defaultTransactionManager") public class DefaultBlacklistedSiteService implements BlacklistedSiteService { @Autowired diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java index cb4b1dc390..00863745a3 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java @@ -1,34 +1,39 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; +import static org.mitre.openid.connect.request.ConnectRequestParameters.MAX_AGE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.NONCE; + import java.util.Date; import java.util.Map; import java.util.Set; import java.util.UUID; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; -import org.mitre.jwt.signer.service.impl.JWKSetCacheService; -import org.mitre.jwt.signer.service.impl.SymmetricCacheService; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.ClientKeyCacheService; +import org.mitre.jwt.signer.service.impl.SymmetricKeyJWTValidatorCacheService; import org.mitre.oauth2.model.AuthenticationHolderEntity; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.service.OAuth2TokenEntityService; import org.mitre.oauth2.service.SystemScopeService; import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.mitre.openid.connect.service.OIDCTokenService; @@ -49,6 +54,7 @@ import com.google.common.collect.Sets; import com.nimbusds.jose.Algorithm; import com.nimbusds.jose.JWEHeader; +import com.nimbusds.jose.JWEObject; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.util.Base64URL; @@ -57,20 +63,22 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.PlainJWT; import com.nimbusds.jwt.SignedJWT; - /** * Default implementation of service to create specialty OpenID Connect tokens. - * + * * @author Amanda Anganes * */ @Service public class DefaultOIDCTokenService implements OIDCTokenService { - Logger logger = LoggerFactory.getLogger(DefaultOIDCTokenService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultOIDCTokenService.class); @Autowired - private JwtSigningAndValidationService jwtService; + private JWTSigningAndValidationService jwtService; @Autowired private AuthenticationHolderRepository authenticationHolderRepository; @@ -79,13 +87,16 @@ public class DefaultOIDCTokenService implements OIDCTokenService { private ConfigurationPropertiesBean configBean; @Autowired - private JWKSetCacheService encrypters; + private ClientKeyCacheService encrypters; @Autowired - private SymmetricCacheService symmetricCacheService; + private SymmetricKeyJWTValidatorCacheService symmetricCacheService; + + @Autowired + private OAuth2TokenEntityService tokenService; @Override - public OAuth2AccessTokenEntity createIdToken(ClientDetailsEntity client, OAuth2Request request, Date issueTime, String sub, OAuth2AccessTokenEntity accessToken) { + public JWT createIdToken(ClientDetailsEntity client, OAuth2Request request, Date issueTime, String sub, OAuth2AccessTokenEntity accessToken) { JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm(); @@ -94,35 +105,42 @@ public OAuth2AccessTokenEntity createIdToken(ClientDetailsEntity client, OAuth2R } - OAuth2AccessTokenEntity idTokenEntity = new OAuth2AccessTokenEntity(); - JWTClaimsSet idClaims = new JWTClaimsSet(); + JWT idToken = null; + + JWTClaimsSet.Builder idClaims = new JWTClaimsSet.Builder(); // if the auth time claim was explicitly requested OR if the client always wants the auth time, put it in - if (request.getExtensions().containsKey("max_age") + if (request.getExtensions().containsKey(MAX_AGE) || (request.getExtensions().containsKey("idtoken")) // TODO: parse the ID Token claims (#473) -- for now assume it could be in there || (client.getRequireAuthTime() != null && client.getRequireAuthTime())) { - Date authTime = (Date) request.getExtensions().get(AuthenticationTimeStamper.AUTH_TIMESTAMP); - if (authTime != null) { - idClaims.setClaim("auth_time", authTime.getTime() / 1000); + if (request.getExtensions().get(AuthenticationTimeStamper.AUTH_TIMESTAMP) != null) { + + Long authTimestamp = Long.parseLong((String) request.getExtensions().get(AuthenticationTimeStamper.AUTH_TIMESTAMP)); + if (authTimestamp != null) { + idClaims.claim("auth_time", authTimestamp / 1000L); + } + } else { + // we couldn't find the timestamp! + logger.warn("Unable to find authentication timestamp! There is likely something wrong with the configuration."); } } - idClaims.setIssueTime(issueTime); + idClaims.issueTime(issueTime); if (client.getIdTokenValiditySeconds() != null) { Date expiration = new Date(System.currentTimeMillis() + (client.getIdTokenValiditySeconds() * 1000L)); - idClaims.setExpirationTime(expiration); - idTokenEntity.setExpiration(expiration); + idClaims.expirationTime(expiration); } - idClaims.setIssuer(configBean.getIssuer()); - idClaims.setSubject(sub); - idClaims.setAudience(Lists.newArrayList(client.getClientId())); + idClaims.issuer(configBean.getIssuer()); + idClaims.subject(sub); + idClaims.audience(Lists.newArrayList(client.getClientId())); + idClaims.jwtID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it - String nonce = (String)request.getExtensions().get("nonce"); + String nonce = (String)request.getExtensions().get(NONCE); if (!Strings.isNullOrEmpty(nonce)) { - idClaims.setCustomClaim("nonce", nonce); + idClaims.claim("nonce", nonce); } Set responseTypes = request.getResponseTypes(); @@ -130,68 +148,67 @@ public OAuth2AccessTokenEntity createIdToken(ClientDetailsEntity client, OAuth2R if (responseTypes.contains("token")) { // calculate the token hash Base64URL at_hash = IdTokenHashUtils.getAccessTokenHash(signingAlg, accessToken); - idClaims.setClaim("at_hash", at_hash); + idClaims.claim("at_hash", at_hash); } + addCustomIdTokenClaims(idClaims, client, request, sub, accessToken); + if (client.getIdTokenEncryptedResponseAlg() != null && !client.getIdTokenEncryptedResponseAlg().equals(Algorithm.NONE) && client.getIdTokenEncryptedResponseEnc() != null && !client.getIdTokenEncryptedResponseEnc().equals(Algorithm.NONE) - && !Strings.isNullOrEmpty(client.getJwksUri())) { + && (!Strings.isNullOrEmpty(client.getJwksUri()) || client.getJwks() != null)) { - JwtEncryptionAndDecryptionService encrypter = encrypters.getEncrypter(client.getJwksUri()); + JWTEncryptionAndDecryptionService encrypter = encrypters.getEncrypter(client); if (encrypter != null) { - EncryptedJWT idToken = new EncryptedJWT(new JWEHeader(client.getIdTokenEncryptedResponseAlg(), client.getIdTokenEncryptedResponseEnc()), idClaims); - - encrypter.encryptJwt(idToken); + idToken = new EncryptedJWT(new JWEHeader(client.getIdTokenEncryptedResponseAlg(), client.getIdTokenEncryptedResponseEnc()), idClaims.build()); - idTokenEntity.setJwt(idToken); + encrypter.encryptJwt((JWEObject) idToken); } else { logger.error("Couldn't find encrypter for client: " + client.getClientId()); } } else { - - JWT idToken; - - if (signingAlg.equals(JWSAlgorithm.NONE)) { + + if (signingAlg.equals(Algorithm.NONE)) { // unsigned ID token - idToken = new PlainJWT(idClaims); + idToken = new PlainJWT(idClaims.build()); } else { // signed ID token - idToken = new SignedJWT(new JWSHeader(signingAlg), idClaims); - + if (signingAlg.equals(JWSAlgorithm.HS256) || signingAlg.equals(JWSAlgorithm.HS384) || signingAlg.equals(JWSAlgorithm.HS512)) { - JwtSigningAndValidationService signer = symmetricCacheService.getSymmetricValidtor(client); - + + JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null, + jwtService.getDefaultSignerKeyId(), + null, null); + idToken = new SignedJWT(header, idClaims.build()); + + JWTSigningAndValidationService signer = symmetricCacheService.getSymmetricValidtor(client); + // sign it with the client's secret signer.signJwt((SignedJWT) idToken); } else { - + idClaims.claim("kid", jwtService.getDefaultSignerKeyId()); + + JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null, + jwtService.getDefaultSignerKeyId(), + null, null); + + idToken = new SignedJWT(header, idClaims.build()); + // sign it with the server's key jwtService.signJwt((SignedJWT) idToken); } } - - idTokenEntity.setJwt(idToken); } - idTokenEntity.setAuthenticationHolder(accessToken.getAuthenticationHolder()); - - // create a scope set with just the special "id-token" scope - //Set idScopes = new HashSet(token.getScope()); // this would copy the original token's scopes in, we don't really want that - Set idScopes = Sets.newHashSet(SystemScopeService.ID_TOKEN_SCOPE); - idTokenEntity.setScope(idScopes); - - idTokenEntity.setClient(accessToken.getClient()); - - return idTokenEntity; + return idToken; } /** @@ -202,72 +219,73 @@ public OAuth2AccessTokenEntity createIdToken(ClientDetailsEntity client, OAuth2R @Override public OAuth2AccessTokenEntity createRegistrationAccessToken(ClientDetailsEntity client) { - Map authorizationParameters = Maps.newHashMap(); - OAuth2Request clientAuth = new OAuth2Request(authorizationParameters, client.getClientId(), - Sets.newHashSet(new SimpleGrantedAuthority("ROLE_CLIENT")), true, - Sets.newHashSet(SystemScopeService.REGISTRATION_TOKEN_SCOPE), null, null, null, null); - OAuth2Authentication authentication = new OAuth2Authentication(clientAuth, null); - - OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); - token.setClient(client); - token.setScope(Sets.newHashSet(SystemScopeService.REGISTRATION_TOKEN_SCOPE)); - - AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); - authHolder.setAuthentication(authentication); - authHolder = authenticationHolderRepository.save(authHolder); - token.setAuthenticationHolder(authHolder); - - JWTClaimsSet claims = new JWTClaimsSet(); + return createAssociatedToken(client, Sets.newHashSet(SystemScopeService.REGISTRATION_TOKEN_SCOPE)); - claims.setAudience(Lists.newArrayList(client.getClientId())); - claims.setIssuer(configBean.getIssuer()); - claims.setIssueTime(new Date()); - claims.setExpirationTime(token.getExpiration()); - claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it - - JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm(); - SignedJWT signed = new SignedJWT(new JWSHeader(signingAlg), claims); - - jwtService.signJwt(signed); - - token.setJwt(signed); - - return token; } /** * @param client * @return - * @throws AuthenticationException */ @Override public OAuth2AccessTokenEntity createResourceAccessToken(ClientDetailsEntity client) { + return createAssociatedToken(client, Sets.newHashSet(SystemScopeService.RESOURCE_TOKEN_SCOPE)); + + } + + @Override + public OAuth2AccessTokenEntity rotateRegistrationAccessTokenForClient(ClientDetailsEntity client) { + // revoke any previous tokens + OAuth2AccessTokenEntity oldToken = tokenService.getRegistrationAccessTokenForClient(client); + if (oldToken != null) { + Set scope = oldToken.getScope(); + tokenService.revokeAccessToken(oldToken); + return createAssociatedToken(client, scope); + } else { + return null; + } + + } + + private OAuth2AccessTokenEntity createAssociatedToken(ClientDetailsEntity client, Set scope) { + + // revoke any previous tokens that might exist, just to be sure + OAuth2AccessTokenEntity oldToken = tokenService.getRegistrationAccessTokenForClient(client); + if (oldToken != null) { + tokenService.revokeAccessToken(oldToken); + } + + // create a new token + Map authorizationParameters = Maps.newHashMap(); OAuth2Request clientAuth = new OAuth2Request(authorizationParameters, client.getClientId(), Sets.newHashSet(new SimpleGrantedAuthority("ROLE_CLIENT")), true, - Sets.newHashSet(SystemScopeService.RESOURCE_TOKEN_SCOPE), null, null, null, null); + scope, null, null, null, null); OAuth2Authentication authentication = new OAuth2Authentication(clientAuth, null); OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); token.setClient(client); - token.setScope(Sets.newHashSet(SystemScopeService.RESOURCE_TOKEN_SCOPE)); + token.setScope(scope); AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); authHolder.setAuthentication(authentication); authHolder = authenticationHolderRepository.save(authHolder); token.setAuthenticationHolder(authHolder); - JWTClaimsSet claims = new JWTClaimsSet(); - - claims.setAudience(Lists.newArrayList(client.getClientId())); - claims.setIssuer(configBean.getIssuer()); - claims.setIssueTime(new Date()); - claims.setExpirationTime(token.getExpiration()); - claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .audience(Lists.newArrayList(client.getClientId())) + .issuer(configBean.getIssuer()) + .issueTime(new Date()) + .expirationTime(token.getExpiration()) + .jwtID(UUID.randomUUID().toString()) // set a random NONCE in the middle of it + .build(); JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm(); - SignedJWT signed = new SignedJWT(new JWSHeader(signingAlg), claims); + JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null, + jwtService.getDefaultSignerKeyId(), + null, null); + SignedJWT signed = new SignedJWT(header, claims); jwtService.signJwt(signed); @@ -293,14 +311,14 @@ public void setConfigBean(ConfigurationPropertiesBean configBean) { /** * @return the jwtService */ - public JwtSigningAndValidationService getJwtService() { + public JWTSigningAndValidationService getJwtService() { return jwtService; } /** * @param jwtService the jwtService to set */ - public void setJwtService(JwtSigningAndValidationService jwtService) { + public void setJwtService(JWTSigningAndValidationService jwtService) { this.jwtService = jwtService; } @@ -319,4 +337,18 @@ public void setAuthenticationHolderRepository( this.authenticationHolderRepository = authenticationHolderRepository; } + /** + * Hook for subclasses that allows adding custom claims to the JWT + * that will be used as id token. + * @param idClaims the builder holding the current claims + * @param client information about the requesting client + * @param request request that caused the id token to be created + * @param sub subject auf the id token + * @param accessToken the access token + * @param authentication current authentication + */ + protected void addCustomIdTokenClaims(JWTClaimsSet.Builder idClaims, ClientDetailsEntity client, OAuth2Request request, + String sub, OAuth2AccessTokenEntity accessToken) { + } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultScopeClaimTranslationService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultScopeClaimTranslationService.java index 14e3d96d55..5c0637a5e2 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultScopeClaimTranslationService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultScopeClaimTranslationService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,19 +18,17 @@ package org.mitre.openid.connect.service.impl; import java.util.HashSet; -import java.util.Map; import java.util.Set; import org.mitre.openid.connect.service.ScopeClaimTranslationService; import org.springframework.stereotype.Service; import com.google.common.collect.HashMultimap; -import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; /** * Service to map scopes to claims, and claims to Java field names - * + * * @author Amanda Anganes * */ @@ -37,13 +36,11 @@ public class DefaultScopeClaimTranslationService implements ScopeClaimTranslationService { private SetMultimap scopesToClaims = HashMultimap.create(); - private Map claimsToFields = Maps.newHashMap(); /** * Default constructor; initializes scopesToClaims map */ public DefaultScopeClaimTranslationService() { - scopesToClaims.put("openid", "sub"); scopesToClaims.put("profile", "name"); @@ -56,9 +53,9 @@ public DefaultScopeClaimTranslationService() { scopesToClaims.put("profile", "picture"); scopesToClaims.put("profile", "website"); scopesToClaims.put("profile", "gender"); - scopesToClaims.put("profile", "zone_info"); + scopesToClaims.put("profile", "zoneinfo"); scopesToClaims.put("profile", "locale"); - scopesToClaims.put("profile", "updated_time"); + scopesToClaims.put("profile", "updated_at"); scopesToClaims.put("profile", "birthdate"); scopesToClaims.put("email", "email"); @@ -68,32 +65,6 @@ public DefaultScopeClaimTranslationService() { scopesToClaims.put("phone", "phone_number_verified"); scopesToClaims.put("address", "address"); - - claimsToFields.put("sub", "sub"); - - claimsToFields.put("name", "name"); - claimsToFields.put("preferred_username", "preferredUsername"); - claimsToFields.put("given_name", "givenName"); - claimsToFields.put("family_name", "familyName"); - claimsToFields.put("middle_name", "middleName"); - claimsToFields.put("nickname", "nickname"); - claimsToFields.put("profile", "profile"); - claimsToFields.put("picture", "picture"); - claimsToFields.put("website", "website"); - claimsToFields.put("gender", "gender"); - claimsToFields.put("zone_info", "zoneinfo"); - claimsToFields.put("locale", "locale"); - claimsToFields.put("updated_time", "updatedTime"); - claimsToFields.put("birthdate", "birthdate"); - - claimsToFields.put("email", "email"); - claimsToFields.put("email_verified", "emailVerified"); - - claimsToFields.put("phone_number", "phoneNumber"); - claimsToFields.put("phone_number_verified", "phoneNumberVerified"); - - claimsToFields.put("address", "address"); - } /* (non-Javadoc) @@ -104,7 +75,7 @@ public Set getClaimsForScope(String scope) { if (scopesToClaims.containsKey(scope)) { return scopesToClaims.get(scope); } else { - return new HashSet(); + return new HashSet<>(); } } @@ -113,19 +84,11 @@ public Set getClaimsForScope(String scope) { */ @Override public Set getClaimsForScopeSet(Set scopes) { - Set result = new HashSet(); + Set result = new HashSet<>(); for (String scope : scopes) { result.addAll(getClaimsForScope(scope)); } return result; } - /* (non-Javadoc) - * @see org.mitre.openid.connect.service.ScopeClaimTranslationService#getFieldNameForClaim(java.lang.String) - */ - @Override - public String getFieldNameForClaim(String claim) { - return claimsToFields.get(claim); - } - } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultStatsService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultStatsService.java index 1cd6d15a7a..2305fc16ea 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultStatsService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultStatsService.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; @@ -26,9 +27,8 @@ import java.util.Set; import java.util.concurrent.TimeUnit; -import org.mitre.oauth2.model.ClientDetailsEntity; -import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.ClientStat; import org.mitre.openid.connect.service.ApprovedSiteService; import org.mitre.openid.connect.service.StatsService; import org.springframework.beans.factory.annotation.Autowired; @@ -36,8 +36,6 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -import com.google.common.collect.HashMultiset; -import com.google.common.collect.Multiset; /** * @author jricher @@ -49,9 +47,6 @@ public class DefaultStatsService implements StatsService { @Autowired private ApprovedSiteService approvedSiteService; - @Autowired - private ClientDetailsEntityService clientService; - // stats cache private Supplier> summaryCache = createSummaryCache(); @@ -65,18 +60,6 @@ public Map get() { }, 10, TimeUnit.MINUTES); } - private Supplier> byClientIdCache = createByClientIdCache(); - - private Supplier> createByClientIdCache() { - return Suppliers.memoizeWithExpiration(new Supplier>() { - @Override - public Map get() { - return computeByClientId(); - } - - }, 10, TimeUnit.MINUTES); - } - @Override public Map getSummaryStats() { return summaryCache.get(); @@ -88,14 +71,14 @@ private Map computeSummaryStats() { Collection allSites = approvedSiteService.getAll(); // process to find number of unique users and sites - Set userIds = new HashSet(); - Set clientIds = new HashSet(); + Set userIds = new HashSet<>(); + Set clientIds = new HashSet<>(); for (ApprovedSite approvedSite : allSites) { userIds.add(approvedSite.getUserId()); clientIds.add(approvedSite.getClientId()); } - Map e = new HashMap(); + Map e = new HashMap<>(); e.put("approvalCount", allSites.size()); e.put("userCount", userIds.size()); @@ -103,55 +86,18 @@ private Map computeSummaryStats() { return e; } - /* (non-Javadoc) - * @see org.mitre.openid.connect.service.StatsService#calculateByClientId() - */ - @Override - public Map getByClientId() { - return byClientIdCache.get(); - } - - private Map computeByClientId() { - // get all approved sites - Collection allSites = approvedSiteService.getAll(); - - Multiset clientIds = HashMultiset.create(); - for (ApprovedSite approvedSite : allSites) { - clientIds.add(approvedSite.getClientId()); - } - - Map counts = getEmptyClientCountMap(); - for (String clientId : clientIds) { - ClientDetailsEntity client = clientService.loadClientByClientId(clientId); - counts.put(client.getId(), clientIds.count(clientId)); - } - - return counts; - } - /* (non-Javadoc) * @see org.mitre.openid.connect.service.StatsService#countForClientId(java.lang.String) */ @Override - public Integer getCountForClientId(Long id) { - - Map counts = getByClientId(); - return counts.get(id); + public ClientStat getCountForClientId(String clientId) { - } + Collection approvedSites = approvedSiteService.getByClientId(clientId); - /** - * Create a new map of all client ids set to zero - * @return - */ - private Map getEmptyClientCountMap() { - Map counts = new HashMap(); - Collection clients = clientService.getAllClients(); - for (ClientDetailsEntity client : clients) { - counts.put(client.getId(), 0); - } + ClientStat stat = new ClientStat(); + stat.setApprovedSiteCount(approvedSites.size()); - return counts; + return stat; } /** @@ -160,7 +106,6 @@ private Map getEmptyClientCountMap() { @Override public void resetCache() { summaryCache = createSummaryCache(); - byClientIdCache = createByClientIdCache(); } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultUserInfoService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultUserInfoService.java index f2eceb45ec..ce830d6492 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultUserInfoService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultUserInfoService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; import org.mitre.oauth2.model.ClientDetailsEntity; @@ -28,9 +29,9 @@ /** * Implementation of the UserInfoService - * + * * @author Michael Joseph Walsh, jricher - * + * */ @Service public class DefaultUserInfoService implements UserInfoService { @@ -44,21 +45,6 @@ public class DefaultUserInfoService implements UserInfoService { @Autowired private PairwiseIdentiferService pairwiseIdentifierService; - @Override - public void save(UserInfo userInfo) { - userInfoRepository.save(userInfo); - } - - @Override - public UserInfo getBySubject(String userId) { - return userInfoRepository.getBySubject(userId); - } - - @Override - public void remove(UserInfo userInfo) { - userInfoRepository.remove(userInfo); - } - @Override public UserInfo getByUsername(String username) { return userInfoRepository.getByUsername(username); @@ -84,4 +70,9 @@ public UserInfo getByUsernameAndClientId(String username, String clientId) { } + @Override + public UserInfo getByEmailAddress(String email) { + return userInfoRepository.getByEmailAddress(email); + } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultUserInfoUserDetailsService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultUserInfoUserDetailsService.java deleted file mode 100644 index cf4a6e660b..0000000000 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultUserInfoUserDetailsService.java +++ /dev/null @@ -1,88 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.openid.connect.service.impl; - -import java.util.ArrayList; -import java.util.List; - -import org.mitre.openid.connect.model.UserInfo; -import org.mitre.openid.connect.repository.UserInfoRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -/** - * A UserDetailsService backed by a UserInfoRepository. - * - * @author jricher - * - */ -@Service("userInfoUserDetailsService") -public class DefaultUserInfoUserDetailsService implements UserDetailsService { - - @Autowired - private UserInfoRepository repository; - - public static final GrantedAuthority ROLE_USER = new SimpleGrantedAuthority("ROLE_USER"); - public static final GrantedAuthority ROLE_ADMIN = new SimpleGrantedAuthority("ROLE_ADMIN"); - - private List admins = new ArrayList(); - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - UserInfo userInfo = repository.getByUsername(username); - - if (userInfo != null) { - - // TODO: make passwords configurable? part of object? - String password = "password"; - - List authorities = new ArrayList(); - authorities.add(ROLE_USER); - - if (admins != null && admins.contains(username)) { - authorities.add(ROLE_ADMIN); - } - - // TODO: this should really be our own UserDetails wrapper class, shouldn't it? - User user = new User(userInfo.getSub(), password, authorities); - return user; - } else { - throw new UsernameNotFoundException("Could not find username: " + username); - } - } - - /** - * @return the admins - */ - public List getAdmins() { - return admins; - } - - /** - * @param admins the admins to set - */ - public void setAdmins(List admins) { - this.admins = admins; - } - -} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultWhitelistedSiteService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultWhitelistedSiteService.java index e11c808f43..3c530b5135 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultWhitelistedSiteService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultWhitelistedSiteService.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; import java.util.Collection; @@ -27,12 +28,12 @@ /** * Implementation of the WhitelistedSiteService - * + * * @author Michael Joseph Walsh, aanganes - * + * */ @Service -@Transactional +@Transactional(value="defaultTransactionManager") public class DefaultWhitelistedSiteService implements WhitelistedSiteService { @Autowired @@ -66,11 +67,6 @@ public WhitelistedSite getByClientId(String clientId) { return repository.getByClientId(clientId); } - @Override - public Collection getByCreator(String creatorId) { - return repository.getByCreator(creatorId); - } - @Override public WhitelistedSite update(WhitelistedSite oldWhitelistedSite, WhitelistedSite whitelistedSite) { if (oldWhitelistedSite == null || whitelistedSite == null) { diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DummyResourceSetService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DummyResourceSetService.java new file mode 100644 index 0000000000..ad60243c10 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DummyResourceSetService.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import java.util.Collection; +import java.util.Collections; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ResourceSetService; +import org.springframework.stereotype.Service; + +/** + * Dummy resource set service that doesn't do anything; acts as a stub for the + * introspection service when the UMA functionality is disabled. + * + * @author jricher + * + */ +@Service +public class DummyResourceSetService implements ResourceSetService { + + @Override + public ResourceSet saveNew(ResourceSet rs) { + throw new UnsupportedOperationException(); + } + + @Override + public ResourceSet getById(Long id) { + throw new UnsupportedOperationException(); + } + + @Override + public ResourceSet update(ResourceSet oldRs, ResourceSet newRs) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(ResourceSet rs) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getAllForOwner(String owner) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getAllForOwnerAndClient(String owner, String authClientId) { + return Collections.emptySet(); + } + + @Override + public Collection getAllForClient(ClientDetailsEntity client) { + return Collections.emptySet(); + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/InMemoryClientLogoLoadingService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/InMemoryClientLogoLoadingService.java new file mode 100644 index 0000000000..e16d0692a4 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/InMemoryClientLogoLoadingService.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClientBuilder; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.openid.connect.model.CachedImage; +import org.mitre.openid.connect.service.ClientLogoLoadingService; +import org.springframework.stereotype.Service; + +import com.google.common.base.Strings; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.UncheckedExecutionException; + +/** + * @author jricher + * + */ +@Service("inMemoryClientLogoLoadingService") +public class InMemoryClientLogoLoadingService implements ClientLogoLoadingService { + + private LoadingCache cache; + + public InMemoryClientLogoLoadingService() { + this(HttpClientBuilder.create().useSystemProperties().build()); + } + + /** + * + */ + public InMemoryClientLogoLoadingService(HttpClient httpClient) { + + cache = CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterAccess(14, TimeUnit.DAYS) + .build(new ClientLogoFetcher(httpClient)); + + } + + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.ClientLogoLoadingService#getLogo(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public CachedImage getLogo(ClientDetailsEntity client) { + try { + if (client != null && !Strings.isNullOrEmpty(client.getLogoUri())) { + return cache.get(client); + } else { + return null; + } + } catch (UncheckedExecutionException | ExecutionException e) { + return null; + } + } + + /** + * @author jricher + * + */ + public class ClientLogoFetcher extends CacheLoader { + private HttpClient httpClient; + + public ClientLogoFetcher() { + this(HttpClientBuilder.create().useSystemProperties().build()); + } + + public ClientLogoFetcher(HttpClient httpClient) { + this.httpClient = httpClient; + } + + /* (non-Javadoc) + * @see com.google.common.cache.CacheLoader#load(java.lang.Object) + */ + @Override + public CachedImage load(ClientDetailsEntity key) throws Exception { + try { + HttpResponse response = httpClient.execute(new HttpGet(key.getLogoUri())); + + HttpEntity entity = response.getEntity(); + + CachedImage image = new CachedImage(); + + image.setContentType(entity.getContentType().getValue()); + image.setLength(entity.getContentLength()); + image.setData(IOUtils.toByteArray(entity.getContent())); + + return image; + } catch (IOException e) { + throw new IllegalArgumentException("Unable to load client image."); + } + } + + } + + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataServiceSupport.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataServiceSupport.java new file mode 100644 index 0000000000..c4e787c9eb --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataServiceSupport.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import java.text.ParseException; +import java.util.Date; +import java.util.Locale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.datetime.DateFormatter; + +public abstract class MITREidDataServiceSupport { + private final DateFormatter dateFormatter; + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(MITREidDataServiceSupport.class); + + public MITREidDataServiceSupport() { + dateFormatter = new DateFormatter(); + dateFormatter.setIso(ISO.DATE_TIME); + } + + protected Date utcToDate(String value) { + if (value == null) { + return null; + } + try { + return dateFormatter.parse(value, Locale.ENGLISH); + } catch (ParseException ex) { + logger.error("Unable to parse datetime {}", value, ex); + } + return null; + } + + protected String toUTCString(Date value) { + if (value == null) { + return null; + } + return dateFormatter.print(value, Locale.ENGLISH); + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_0.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_0.java new file mode 100644 index 0000000000..57c3257726 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_0.java @@ -0,0 +1,906 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mitre.util.JsonUtils.readMap; +import static org.mitre.util.JsonUtils.readSet; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AppType; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SavedUserAuthentication; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mitre.openid.connect.service.MITREidDataServiceExtension; +import org.mitre.openid.connect.service.MITREidDataServiceMaps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.stereotype.Service; + +import com.google.common.collect.Sets; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.JWTParser; +/** + * + * Data service to import MITREid 1.0 configuration. + * + * @author jricher + * @author arielak + */ +@Service +@SuppressWarnings(value = {"unchecked"}) +public class MITREidDataService_1_0 extends MITREidDataServiceSupport implements MITREidDataService { + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(MITREidDataService_1_0.class); + @Autowired + private OAuth2ClientRepository clientRepository; + @Autowired + private ApprovedSiteRepository approvedSiteRepository; + @Autowired + private WhitelistedSiteRepository wlSiteRepository; + @Autowired + private BlacklistedSiteRepository blSiteRepository; + @Autowired + private AuthenticationHolderRepository authHolderRepository; + @Autowired + private OAuth2TokenRepository tokenRepository; + @Autowired + private SystemScopeRepository sysScopeRepository; + @Autowired(required = false) + private List extensions = Collections.emptyList(); + + private MITREidDataServiceMaps maps = new MITREidDataServiceMaps(); + + private static final String THIS_VERSION = MITREID_CONNECT_1_0; + + @Override + public boolean supportsVersion(String version) { + return THIS_VERSION.equals(version); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#export(com.google.gson.stream.JsonWriter) + */ + + @Override + public void exportData(JsonWriter writer) throws IOException { + throw new UnsupportedOperationException("Can not export 1.0 format from this version."); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#importData(com.google.gson.stream.JsonReader) + */ + @Override + public void importData(JsonReader reader) throws IOException { + + logger.info("Reading configuration for 1.0"); + + // this *HAS* to start as an object + reader.beginObject(); + + while (reader.hasNext()) { + JsonToken tok = reader.peek(); + switch (tok) { + case NAME: + String name = reader.nextName(); + // find out which member it is + if (name.equals(CLIENTS)) { + readClients(reader); + } else if (name.equals(GRANTS)) { + readGrants(reader); + } else if (name.equals(WHITELISTEDSITES)) { + readWhitelistedSites(reader); + } else if (name.equals(BLACKLISTEDSITES)) { + readBlacklistedSites(reader); + } else if (name.equals(AUTHENTICATIONHOLDERS)) { + readAuthenticationHolders(reader); + } else if (name.equals(ACCESSTOKENS)) { + readAccessTokens(reader); + } else if (name.equals(REFRESHTOKENS)) { + readRefreshTokens(reader); + } else if (name.equals(SYSTEMSCOPES)) { + readSystemScopes(reader); + } else { + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.importExtensionData(name, reader); + break; + } + } + } + // unknown token, skip it + reader.skipValue(); + } + break; + case END_OBJECT: + // the object ended, we're done here + reader.endObject(); + continue; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; } + } + fixObjectReferences(); + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.fixExtensionObjectReferences(maps); + break; + } + } + maps.clearAll(); + } + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readRefreshTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2RefreshTokenEntity token = new OAuth2RefreshTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("expiration")) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals("value")) { + String value = reader.nextString(); + try { + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals("clientId")) { + clientId = reader.nextString(); + } else if (name.equals("authenticationHolderId")) { + authHolderId = reader.nextLong(); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveRefreshToken(token).getId(); + maps.getRefreshTokenToClientRefs().put(currentId, clientId); + maps.getRefreshTokenToAuthHolderRefs().put(currentId, authHolderId); + maps.getRefreshTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read refresh token {}", currentId); + } + reader.endArray(); + logger.info("Done reading refresh tokens"); + } + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readAccessTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + Long refreshTokenId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("expiration")) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals("value")) { + String value = reader.nextString(); + try { + // all tokens are JWTs + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals("clientId")) { + clientId = reader.nextString(); + } else if (name.equals("authenticationHolderId")) { + authHolderId = reader.nextLong(); + } else if (name.equals("refreshTokenId")) { + refreshTokenId = reader.nextLong(); + } else if (name.equals("scope")) { + Set scope = readSet(reader); + token.setScope(scope); + } else if (name.equals("type")) { + token.setTokenType(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveAccessToken(token).getId(); + maps.getAccessTokenToClientRefs().put(currentId, clientId); + maps.getAccessTokenToAuthHolderRefs().put(currentId, authHolderId); + if (refreshTokenId != null) { + maps.getAccessTokenToRefreshTokenRefs().put(currentId, refreshTokenId); + } + maps.getAccessTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read access token {}", currentId); + } + reader.endArray(); + logger.info("Done reading access tokens"); + } + /** + * @param reader + * @throws IOException + */ + private void readAuthenticationHolders(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + AuthenticationHolderEntity ahe = new AuthenticationHolderEntity(); + reader.beginObject(); + Long currentId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("ownerId")) { + //not needed + reader.skipValue(); + } else if (name.equals("authentication")) { + OAuth2Request clientAuthorization = null; + Authentication userAuthentication = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String subName = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (subName.equals("clientAuthorization")) { + clientAuthorization = readAuthorizationRequest(reader); + } else if (subName.equals("userAuthentication")) { + // skip binary encoded version + reader.skipValue(); + + } else if (subName.equals("savedUserAuthentication")) { + userAuthentication = readSavedUserAuthentication(reader); + + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + OAuth2Authentication auth = new OAuth2Authentication(clientAuthorization, userAuthentication); + ahe.setAuthentication(auth); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = authHolderRepository.save(ahe).getId(); + maps.getAuthHolderOldToNewIdMap().put(currentId, newId); + logger.debug("Read authentication holder {}", currentId); + } + reader.endArray(); + logger.info("Done reading authentication holders"); + } + + //used by readAuthenticationHolders + private OAuth2Request readAuthorizationRequest(JsonReader reader) throws IOException { + Set scope = new LinkedHashSet<>(); + Set resourceIds = new HashSet<>(); + boolean approved = false; + Collection authorities = new HashSet<>(); + Map authorizationParameters = new HashMap<>(); + Set responseTypes = new HashSet<>(); + String redirectUri = null; + String clientId = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("authorizationParameters")) { + authorizationParameters = readMap(reader); + } else if (name.equals("approvalParameters")) { + reader.skipValue(); + } else if (name.equals("clientId")) { + clientId = reader.nextString(); + } else if (name.equals("scope")) { + scope = readSet(reader); + } else if (name.equals("resourceIds")) { + resourceIds = readSet(reader); + } else if (name.equals("authorities")) { + Set authorityStrs = readSet(reader); + authorities = new HashSet<>(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + } else if (name.equals("approved")) { + approved = reader.nextBoolean(); + } else if (name.equals("denied")) { + if (approved == false) { + approved = !reader.nextBoolean(); + } + } else if (name.equals("redirectUri")) { + redirectUri = reader.nextString(); + } else if (name.equals("responseTypes")) { + responseTypes = readSet(reader); + } else { + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + return new OAuth2Request(authorizationParameters, clientId, authorities, approved, scope, resourceIds, redirectUri, responseTypes, null); + } + + /** + * @param reader + * @return + * @throws IOException + */ + private SavedUserAuthentication readSavedUserAuthentication(JsonReader reader) throws IOException { + SavedUserAuthentication savedUserAuth = new SavedUserAuthentication(); + reader.beginObject(); + + while (reader.hasNext()) { + switch(reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("name")) { + savedUserAuth.setName(reader.nextString()); + } else if (name.equals("sourceClass")) { + savedUserAuth.setSourceClass(reader.nextString()); + } else if (name.equals("authenticated")) { + savedUserAuth.setAuthenticated(reader.nextBoolean()); + } else if (name.equals("authorities")) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + savedUserAuth.setAuthorities(authorities); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + + reader.endObject(); + return savedUserAuth; + } + + /** + * @param reader + * @throws IOException + */ + private void readGrants(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ApprovedSite site = new ApprovedSite(); + Long currentId = null; + Long whitelistedSiteId = null; + Set tokenIds = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("accessDate")) { + Date date = utcToDate(reader.nextString()); + site.setAccessDate(date); + } else if (name.equals("clientId")) { + site.setClientId(reader.nextString()); + } else if (name.equals("creationDate")) { + Date date = utcToDate(reader.nextString()); + site.setCreationDate(date); + } else if (name.equals("timeoutDate")) { + Date date = utcToDate(reader.nextString()); + site.setTimeoutDate(date); + } else if (name.equals("userId")) { + site.setUserId(reader.nextString()); + } else if (name.equals("allowedScopes")) { + Set allowedScopes = readSet(reader); + site.setAllowedScopes(allowedScopes); + } else if (name.equals("whitelistedSiteId")) { + whitelistedSiteId = reader.nextLong(); + } else if (name.equals("approvedAccessTokens")) { + tokenIds = readSet(reader); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = approvedSiteRepository.save(site).getId(); + maps.getGrantOldToNewIdMap().put(currentId, newId); + if (whitelistedSiteId != null) { + logger.debug("Ignoring whitelisted site marker on approved site."); + } + if (tokenIds != null) { + maps.getGrantToAccessTokensRefs().put(currentId, tokenIds); + } + logger.debug("Read grant {}", currentId); + } + reader.endArray(); + logger.info("Done reading grants"); + } + /** + * @param reader + * @throws IOException + */ + private void readWhitelistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + WhitelistedSite wlSite = new WhitelistedSite(); + Long currentId = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("clientId")) { + wlSite.setClientId(reader.nextString()); + } else if (name.equals("creatorUserId")) { + wlSite.setCreatorUserId(reader.nextString()); + } else if (name.equals("allowedScopes")) { + Set allowedScopes = readSet(reader); + wlSite.setAllowedScopes(allowedScopes); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = wlSiteRepository.save(wlSite).getId(); + maps.getWhitelistedSiteOldToNewIdMap().put(currentId, newId); + } + reader.endArray(); + logger.info("Done reading whitelisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readBlacklistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + BlacklistedSite blSite = new BlacklistedSite(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals("id")) { + reader.skipValue(); + } else if (name.equals("uri")) { + blSite.setUri(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + blSiteRepository.save(blSite); + } + reader.endArray(); + logger.info("Done reading blacklisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readClients(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ClientDetailsEntity client = new ClientDetailsEntity(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("clientId")) { + client.setClientId(reader.nextString()); + } else if (name.equals("resourceIds")) { + Set resourceIds = readSet(reader); + client.setResourceIds(resourceIds); + } else if (name.equals("secret")) { + client.setClientSecret(reader.nextString()); + } else if (name.equals("scope")) { + Set scope = readSet(reader); + client.setScope(scope); + } else if (name.equals("authorities")) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet<>(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + client.setAuthorities(authorities); + } else if (name.equals("accessTokenValiditySeconds")) { + client.setAccessTokenValiditySeconds(reader.nextInt()); + } else if (name.equals("refreshTokenValiditySeconds")) { + client.setRefreshTokenValiditySeconds(reader.nextInt()); + } else if (name.equals("redirectUris")) { + Set redirectUris = readSet(reader); + client.setRedirectUris(redirectUris); + } else if (name.equals("name")) { + client.setClientName(reader.nextString()); + } else if (name.equals("uri")) { + client.setClientUri(reader.nextString()); + } else if (name.equals("logoUri")) { + client.setLogoUri(reader.nextString()); + } else if (name.equals("contacts")) { + Set contacts = readSet(reader); + client.setContacts(contacts); + } else if (name.equals("tosUri")) { + client.setTosUri(reader.nextString()); + } else if (name.equals("tokenEndpointAuthMethod")) { + AuthMethod am = AuthMethod.getByValue(reader.nextString()); + client.setTokenEndpointAuthMethod(am); + } else if (name.equals("grantTypes")) { + Set grantTypes = readSet(reader); + client.setGrantTypes(grantTypes); + } else if (name.equals("responseTypes")) { + Set responseTypes = readSet(reader); + client.setResponseTypes(responseTypes); + } else if (name.equals("policyUri")) { + client.setPolicyUri(reader.nextString()); + } else if (name.equals("applicationType")) { + AppType appType = AppType.getByValue(reader.nextString()); + client.setApplicationType(appType); + } else if (name.equals("sectorIdentifierUri")) { + client.setSectorIdentifierUri(reader.nextString()); + } else if (name.equals("subjectType")) { + SubjectType st = SubjectType.getByValue(reader.nextString()); + client.setSubjectType(st); + } else if (name.equals("jwks_uri")) { + client.setJwksUri(reader.nextString()); + } else if (name.equals("requestObjectSigningAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setRequestObjectSigningAlg(alg); + } else if (name.equals("userInfoEncryptedResponseAlg")) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setUserInfoEncryptedResponseAlg(alg); + } else if (name.equals("userInfoEncryptedResponseEnc")) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setUserInfoEncryptedResponseEnc(alg); + } else if (name.equals("userInfoSignedResponseAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setUserInfoSignedResponseAlg(alg); + } else if (name.equals("idTokenSignedResonseAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setIdTokenSignedResponseAlg(alg); + } else if (name.equals("idTokenEncryptedResponseAlg")) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setIdTokenEncryptedResponseAlg(alg); + } else if (name.equals("idTokenEncryptedResponseEnc")) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setIdTokenEncryptedResponseEnc(alg); + } else if (name.equals("tokenEndpointAuthSigningAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setTokenEndpointAuthSigningAlg(alg); + } else if (name.equals("defaultMaxAge")) { + client.setDefaultMaxAge(reader.nextInt()); + } else if (name.equals("requireAuthTime")) { + client.setRequireAuthTime(reader.nextBoolean()); + } else if (name.equals("defaultACRValues")) { + Set defaultACRvalues = readSet(reader); + client.setDefaultACRvalues(defaultACRvalues); + } else if (name.equals("initiateLoginUri")) { + client.setInitiateLoginUri(reader.nextString()); + } else if (name.equals("postLogoutRedirectUri")) { + HashSet postLogoutUris = Sets.newHashSet(reader.nextString()); + client.setPostLogoutRedirectUris(postLogoutUris); + } else if (name.equals("requestUris")) { + Set requestUris = readSet(reader); + client.setRequestUris(requestUris); + } else if (name.equals("description")) { + client.setClientDescription(reader.nextString()); + } else if (name.equals("allowIntrospection")) { + client.setAllowIntrospection(reader.nextBoolean()); + } else if (name.equals("reuseRefreshToken")) { + client.setReuseRefreshToken(reader.nextBoolean()); + } else if (name.equals("dynamicallyRegistered")) { + client.setDynamicallyRegistered(reader.nextBoolean()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + clientRepository.saveClient(client); + } + reader.endArray(); + logger.info("Done reading clients"); + } + + /** + * Read the list of system scopes from the reader and insert them into the + * scope repository. + * + * @param reader + * @throws IOException + */ + private void readSystemScopes(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + SystemScope scope = new SystemScope(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("value")) { + scope.setValue(reader.nextString()); + } else if (name.equals("description")) { + scope.setDescription(reader.nextString()); + } else if (name.equals("allowDynReg")) { + // previously "allowDynReg" scopes are now tagged as "not restricted" and vice versa + scope.setRestricted(!reader.nextBoolean()); + } else if (name.equals("defaultScope")) { + scope.setDefaultScope(reader.nextBoolean()); + } else if (name.equals("icon")) { + scope.setIcon(reader.nextString()); + } else { + logger.debug("found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + sysScopeRepository.save(scope); + } + reader.endArray(); + logger.info("Done reading system scopes"); + } + + private void fixObjectReferences() { + for (Long oldRefreshTokenId : maps.getRefreshTokenToClientRefs().keySet()) { + String clientRef = maps.getRefreshTokenToClientRefs().get(oldRefreshTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setClient(client); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldRefreshTokenId : maps.getRefreshTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getRefreshTokenToAuthHolderRefs().get(oldRefreshTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setAuthenticationHolder(authHolder); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToClientRefs().keySet()) { + String clientRef = maps.getAccessTokenToClientRefs().get(oldAccessTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setClient(client); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getAccessTokenToAuthHolderRefs().get(oldAccessTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setAuthenticationHolder(authHolder); + tokenRepository.saveAccessToken(accessToken); + } + maps.getAccessTokenToAuthHolderRefs().clear(); + for (Long oldAccessTokenId : maps.getAccessTokenToRefreshTokenRefs().keySet()) { + Long oldRefreshTokenId = maps.getAccessTokenToRefreshTokenRefs().get(oldAccessTokenId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setRefreshToken(refreshToken); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldGrantId : maps.getGrantToAccessTokensRefs().keySet()) { + Set oldAccessTokenIds = maps.getGrantToAccessTokensRefs().get(oldGrantId); + + Long newGrantId = maps.getGrantOldToNewIdMap().get(oldGrantId); + ApprovedSite site = approvedSiteRepository.getById(newGrantId); + + for(Long oldTokenId : oldAccessTokenIds) { + Long newTokenId = maps.getAccessTokenOldToNewIdMap().get(oldTokenId); + OAuth2AccessTokenEntity token = tokenRepository.getAccessTokenById(newTokenId); + token.setApprovedSite(site); + tokenRepository.saveAccessToken(token); + } + + approvedSiteRepository.save(site); + } + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_1.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_1.java new file mode 100644 index 0000000000..38287ee633 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_1.java @@ -0,0 +1,920 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mitre.util.JsonUtils.readMap; +import static org.mitre.util.JsonUtils.readSet; + +import java.io.IOException; +import java.io.Serializable; +import java.text.ParseException; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AppType; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SavedUserAuthentication; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mitre.openid.connect.service.MITREidDataServiceExtension; +import org.mitre.openid.connect.service.MITREidDataServiceMaps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.stereotype.Service; + +import com.google.common.collect.Sets; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.JWTParser; + +/** + * + * Data service to import MITREid 1.1 configuration. + * + * @author jricher + * @author arielak + */ +@Service +@SuppressWarnings(value = {"unchecked"}) +public class MITREidDataService_1_1 extends MITREidDataServiceSupport implements MITREidDataService { + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(MITREidDataService_1_1.class); + @Autowired + private OAuth2ClientRepository clientRepository; + @Autowired + private ApprovedSiteRepository approvedSiteRepository; + @Autowired + private WhitelistedSiteRepository wlSiteRepository; + @Autowired + private BlacklistedSiteRepository blSiteRepository; + @Autowired + private AuthenticationHolderRepository authHolderRepository; + @Autowired + private OAuth2TokenRepository tokenRepository; + @Autowired + private SystemScopeRepository sysScopeRepository; + @Autowired(required = false) + private List extensions = Collections.emptyList(); + + private static final String THIS_VERSION = MITREID_CONNECT_1_1; + + private MITREidDataServiceMaps maps = new MITREidDataServiceMaps(); + + @Override + public boolean supportsVersion(String version) { + return THIS_VERSION.equals(version); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#export(com.google.gson.stream.JsonWriter) + */ + @Override + public void exportData(JsonWriter writer) throws IOException { + throw new UnsupportedOperationException("Can not export 1.1 format from this version."); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#importData(com.google.gson.stream.JsonReader) + */ + @Override + public void importData(JsonReader reader) throws IOException { + + logger.info("Reading configuration for 1.1"); + + // this *HAS* to start as an object + reader.beginObject(); + + while (reader.hasNext()) { + JsonToken tok = reader.peek(); + switch (tok) { + case NAME: + String name = reader.nextName(); + // find out which member it is + if (name.equals(CLIENTS)) { + readClients(reader); + } else if (name.equals(GRANTS)) { + readGrants(reader); + } else if (name.equals(WHITELISTEDSITES)) { + readWhitelistedSites(reader); + } else if (name.equals(BLACKLISTEDSITES)) { + readBlacklistedSites(reader); + } else if (name.equals(AUTHENTICATIONHOLDERS)) { + readAuthenticationHolders(reader); + } else if (name.equals(ACCESSTOKENS)) { + readAccessTokens(reader); + } else if (name.equals(REFRESHTOKENS)) { + readRefreshTokens(reader); + } else if (name.equals(SYSTEMSCOPES)) { + readSystemScopes(reader); + } else { + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.importExtensionData(name, reader); + break; + } + } + } + // unknown token, skip it + reader.skipValue(); + } + break; + case END_OBJECT: + // the object ended, we're done here + reader.endObject(); + continue; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + fixObjectReferences(); + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.fixExtensionObjectReferences(maps); + break; + } + } + maps.clearAll(); + } + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readRefreshTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2RefreshTokenEntity token = new OAuth2RefreshTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("expiration")) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals("value")) { + String value = reader.nextString(); + try { + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals("clientId")) { + clientId = reader.nextString(); + } else if (name.equals("authenticationHolderId")) { + authHolderId = reader.nextLong(); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveRefreshToken(token).getId(); + maps.getRefreshTokenToClientRefs().put(currentId, clientId); + maps.getRefreshTokenToAuthHolderRefs().put(currentId, authHolderId); + maps.getRefreshTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read refresh token {}", currentId); + } + reader.endArray(); + logger.info("Done reading refresh tokens"); + } + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readAccessTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + Long refreshTokenId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("expiration")) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals("value")) { + String value = reader.nextString(); + try { + // all tokens are JWTs + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals("clientId")) { + clientId = reader.nextString(); + } else if (name.equals("authenticationHolderId")) { + authHolderId = reader.nextLong(); + } else if (name.equals("refreshTokenId")) { + refreshTokenId = reader.nextLong(); + } else if (name.equals("scope")) { + Set scope = readSet(reader); + token.setScope(scope); + } else if (name.equals("type")) { + token.setTokenType(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveAccessToken(token).getId(); + maps.getAccessTokenToClientRefs().put(currentId, clientId); + maps.getAccessTokenToAuthHolderRefs().put(currentId, authHolderId); + if (refreshTokenId != null) { + maps.getAccessTokenToRefreshTokenRefs().put(currentId, refreshTokenId); + } + maps.getAccessTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read access token {}", currentId); + } + reader.endArray(); + logger.info("Done reading access tokens"); + } + /** + * @param reader + * @throws IOException + */ + private void readAuthenticationHolders(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + AuthenticationHolderEntity ahe = new AuthenticationHolderEntity(); + reader.beginObject(); + Long currentId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("ownerId")) { + //not needed + reader.skipValue(); + } else if (name.equals("authentication")) { + OAuth2Request clientAuthorization = null; + Authentication userAuthentication = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String subName = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); // skip null values + } else if (subName.equals("clientAuthorization")) { + clientAuthorization = readAuthorizationRequest(reader); + } else if (subName.equals("userAuthentication")) { + // skip binary encoded version + reader.skipValue(); + + } else if (subName.equals("savedUserAuthentication")) { + userAuthentication = readSavedUserAuthentication(reader); + + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + OAuth2Authentication auth = new OAuth2Authentication(clientAuthorization, userAuthentication); + ahe.setAuthentication(auth); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = authHolderRepository.save(ahe).getId(); + maps.getAuthHolderOldToNewIdMap().put(currentId, newId); + logger.debug("Read authentication holder {}", currentId); + } + reader.endArray(); + logger.info("Done reading authentication holders"); + } + + //used by readAuthenticationHolders + private OAuth2Request readAuthorizationRequest(JsonReader reader) throws IOException { + Set scope = new LinkedHashSet<>(); + Set resourceIds = new HashSet<>(); + boolean approved = false; + Collection authorities = new HashSet<>(); + Map requestParameters = new HashMap<>(); + Set responseTypes = new HashSet<>(); + Map extensions = new HashMap<>(); + String redirectUri = null; + String clientId = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("requestParameters")) { + requestParameters = readMap(reader); + } else if (name.equals("clientId")) { + clientId = reader.nextString(); + } else if (name.equals("scope")) { + scope = readSet(reader); + } else if (name.equals("resourceIds")) { + resourceIds = readSet(reader); + } else if (name.equals("authorities")) { + Set authorityStrs = readSet(reader); + authorities = new HashSet<>(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + } else if (name.equals("approved")) { + approved = reader.nextBoolean(); + } else if (name.equals("denied")) { + if (approved == false) { + approved = !reader.nextBoolean(); + } + } else if (name.equals("redirectUri")) { + redirectUri = reader.nextString(); + } else if (name.equals("responseTypes")) { + responseTypes = readSet(reader); + } else if (name.equals("extensions")) { + // skip the binary encoded version + reader.skipValue(); + } else if (name.equals("extensionStrings")) { + Map extEnc = readMap(reader); + for (Entry entry : extEnc.entrySet()) { + extensions.put(entry.getKey(), entry.getValue()); + } + } else { + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + return new OAuth2Request(requestParameters, clientId, authorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions); + } + + /** + * @param reader + * @return + * @throws IOException + */ + private SavedUserAuthentication readSavedUserAuthentication(JsonReader reader) throws IOException { + SavedUserAuthentication savedUserAuth = new SavedUserAuthentication(); + reader.beginObject(); + + while (reader.hasNext()) { + switch(reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("name")) { + savedUserAuth.setName(reader.nextString()); + } else if (name.equals("sourceClass")) { + savedUserAuth.setSourceClass(reader.nextString()); + } else if (name.equals("authenticated")) { + savedUserAuth.setAuthenticated(reader.nextBoolean()); + } else if (name.equals("authorities")) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + savedUserAuth.setAuthorities(authorities); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + + reader.endObject(); + return savedUserAuth; + } + + /** + * @param reader + * @throws IOException + */ + private void readGrants(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ApprovedSite site = new ApprovedSite(); + Long currentId = null; + Long whitelistedSiteId = null; + Set tokenIds = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("accessDate")) { + Date date = utcToDate(reader.nextString()); + site.setAccessDate(date); + } else if (name.equals("clientId")) { + site.setClientId(reader.nextString()); + } else if (name.equals("creationDate")) { + Date date = utcToDate(reader.nextString()); + site.setCreationDate(date); + } else if (name.equals("timeoutDate")) { + Date date = utcToDate(reader.nextString()); + site.setTimeoutDate(date); + } else if (name.equals("userId")) { + site.setUserId(reader.nextString()); + } else if (name.equals("allowedScopes")) { + Set allowedScopes = readSet(reader); + site.setAllowedScopes(allowedScopes); + } else if (name.equals("whitelistedSiteId")) { + whitelistedSiteId = reader.nextLong(); + } else if (name.equals("approvedAccessTokens")) { + tokenIds = readSet(reader); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = approvedSiteRepository.save(site).getId(); + maps.getGrantOldToNewIdMap().put(currentId, newId); + if (whitelistedSiteId != null) { + logger.debug("Ignoring whitelisted site marker on approved site."); + } + if (tokenIds != null) { + maps.getGrantToAccessTokensRefs().put(currentId, tokenIds); + } + logger.debug("Read grant {}", currentId); + } + reader.endArray(); + logger.info("Done reading grants"); + } + /** + * @param reader + * @throws IOException + */ + private void readWhitelistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + WhitelistedSite wlSite = new WhitelistedSite(); + Long currentId = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals("id")) { + currentId = reader.nextLong(); + } else if (name.equals("clientId")) { + wlSite.setClientId(reader.nextString()); + } else if (name.equals("creatorUserId")) { + wlSite.setCreatorUserId(reader.nextString()); + } else if (name.equals("allowedScopes")) { + Set allowedScopes = readSet(reader); + wlSite.setAllowedScopes(allowedScopes); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = wlSiteRepository.save(wlSite).getId(); + maps.getWhitelistedSiteOldToNewIdMap().put(currentId, newId); + } + reader.endArray(); + logger.info("Done reading whitelisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readBlacklistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + BlacklistedSite blSite = new BlacklistedSite(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals("id")) { + reader.skipValue(); + } else if (name.equals("uri")) { + blSite.setUri(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + blSiteRepository.save(blSite); + } + reader.endArray(); + logger.info("Done reading blacklisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readClients(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ClientDetailsEntity client = new ClientDetailsEntity(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("clientId")) { + client.setClientId(reader.nextString()); + } else if (name.equals("resourceIds")) { + Set resourceIds = readSet(reader); + client.setResourceIds(resourceIds); + } else if (name.equals("secret")) { + client.setClientSecret(reader.nextString()); + } else if (name.equals("scope")) { + Set scope = readSet(reader); + client.setScope(scope); + } else if (name.equals("authorities")) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet<>(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + client.setAuthorities(authorities); + } else if (name.equals("accessTokenValiditySeconds")) { + client.setAccessTokenValiditySeconds(reader.nextInt()); + } else if (name.equals("refreshTokenValiditySeconds")) { + client.setRefreshTokenValiditySeconds(reader.nextInt()); + } else if (name.equals("redirectUris")) { + Set redirectUris = readSet(reader); + client.setRedirectUris(redirectUris); + } else if (name.equals("name")) { + client.setClientName(reader.nextString()); + } else if (name.equals("uri")) { + client.setClientUri(reader.nextString()); + } else if (name.equals("logoUri")) { + client.setLogoUri(reader.nextString()); + } else if (name.equals("contacts")) { + Set contacts = readSet(reader); + client.setContacts(contacts); + } else if (name.equals("tosUri")) { + client.setTosUri(reader.nextString()); + } else if (name.equals("tokenEndpointAuthMethod")) { + AuthMethod am = AuthMethod.getByValue(reader.nextString()); + client.setTokenEndpointAuthMethod(am); + } else if (name.equals("grantTypes")) { + Set grantTypes = readSet(reader); + client.setGrantTypes(grantTypes); + } else if (name.equals("responseTypes")) { + Set responseTypes = readSet(reader); + client.setResponseTypes(responseTypes); + } else if (name.equals("policyUri")) { + client.setPolicyUri(reader.nextString()); + } else if (name.equals("applicationType")) { + AppType appType = AppType.getByValue(reader.nextString()); + client.setApplicationType(appType); + } else if (name.equals("sectorIdentifierUri")) { + client.setSectorIdentifierUri(reader.nextString()); + } else if (name.equals("subjectType")) { + SubjectType st = SubjectType.getByValue(reader.nextString()); + client.setSubjectType(st); + } else if (name.equals("jwks_uri")) { + client.setJwksUri(reader.nextString()); + } else if (name.equals("requestObjectSigningAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setRequestObjectSigningAlg(alg); + } else if (name.equals("userInfoEncryptedResponseAlg")) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setUserInfoEncryptedResponseAlg(alg); + } else if (name.equals("userInfoEncryptedResponseEnc")) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setUserInfoEncryptedResponseEnc(alg); + } else if (name.equals("userInfoSignedResponseAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setUserInfoSignedResponseAlg(alg); + } else if (name.equals("idTokenSignedResonseAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setIdTokenSignedResponseAlg(alg); + } else if (name.equals("idTokenEncryptedResponseAlg")) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setIdTokenEncryptedResponseAlg(alg); + } else if (name.equals("idTokenEncryptedResponseEnc")) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setIdTokenEncryptedResponseEnc(alg); + } else if (name.equals("tokenEndpointAuthSigningAlg")) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setTokenEndpointAuthSigningAlg(alg); + } else if (name.equals("defaultMaxAge")) { + client.setDefaultMaxAge(reader.nextInt()); + } else if (name.equals("requireAuthTime")) { + client.setRequireAuthTime(reader.nextBoolean()); + } else if (name.equals("defaultACRValues")) { + Set defaultACRvalues = readSet(reader); + client.setDefaultACRvalues(defaultACRvalues); + } else if (name.equals("initiateLoginUri")) { + client.setInitiateLoginUri(reader.nextString()); + } else if (name.equals("postLogoutRedirectUri")) { + HashSet postLogoutUris = Sets.newHashSet(reader.nextString()); + client.setPostLogoutRedirectUris(postLogoutUris); + } else if (name.equals("requestUris")) { + Set requestUris = readSet(reader); + client.setRequestUris(requestUris); + } else if (name.equals("description")) { + client.setClientDescription(reader.nextString()); + } else if (name.equals("allowIntrospection")) { + client.setAllowIntrospection(reader.nextBoolean()); + } else if (name.equals("reuseRefreshToken")) { + client.setReuseRefreshToken(reader.nextBoolean()); + } else if (name.equals("dynamicallyRegistered")) { + client.setDynamicallyRegistered(reader.nextBoolean()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + clientRepository.saveClient(client); + } + reader.endArray(); + logger.info("Done reading clients"); + } + + /** + * Read the list of system scopes from the reader and insert them into the + * scope repository. + * + * @param reader + * @throws IOException + */ + private void readSystemScopes(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + SystemScope scope = new SystemScope(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals("value")) { + scope.setValue(reader.nextString()); + } else if (name.equals("description")) { + scope.setDescription(reader.nextString()); + } else if (name.equals("allowDynReg")) { + // previously "allowDynReg" scopes are now tagged as "not restricted" and vice versa + scope.setRestricted(!reader.nextBoolean()); + } else if (name.equals("defaultScope")) { + scope.setDefaultScope(reader.nextBoolean()); + } else if (name.equals("structured")) { + logger.warn("Found a structured scope, ignoring structure"); + } else if (name.equals("structuredParameter")) { + logger.warn("Found a structured scope, ignoring structure"); + } else if (name.equals("icon")) { + scope.setIcon(reader.nextString()); + } else { + logger.debug("found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + sysScopeRepository.save(scope); + } + reader.endArray(); + logger.info("Done reading system scopes"); + } + + private void fixObjectReferences() { + for (Long oldRefreshTokenId : maps.getRefreshTokenToClientRefs().keySet()) { + String clientRef = maps.getRefreshTokenToClientRefs().get(oldRefreshTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setClient(client); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldRefreshTokenId : maps.getRefreshTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getRefreshTokenToAuthHolderRefs().get(oldRefreshTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setAuthenticationHolder(authHolder); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToClientRefs().keySet()) { + String clientRef = maps.getAccessTokenToClientRefs().get(oldAccessTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setClient(client); + tokenRepository.saveAccessToken(accessToken); + } + maps.getAccessTokenToClientRefs().clear(); + for (Long oldAccessTokenId : maps.getAccessTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getAccessTokenToAuthHolderRefs().get(oldAccessTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setAuthenticationHolder(authHolder); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToRefreshTokenRefs().keySet()) { + Long oldRefreshTokenId = maps.getAccessTokenToRefreshTokenRefs().get(oldAccessTokenId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setRefreshToken(refreshToken); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldGrantId : maps.getGrantToAccessTokensRefs().keySet()) { + Set oldAccessTokenIds = maps.getGrantToAccessTokensRefs().get(oldGrantId); + + Long newGrantId = maps.getGrantOldToNewIdMap().get(oldGrantId); + ApprovedSite site = approvedSiteRepository.getById(newGrantId); + + for(Long oldTokenId : oldAccessTokenIds) { + Long newTokenId = maps.getAccessTokenOldToNewIdMap().get(oldTokenId); + OAuth2AccessTokenEntity token = tokenRepository.getAccessTokenById(newTokenId); + token.setApprovedSite(site); + tokenRepository.saveAccessToken(token); + } + + approvedSiteRepository.save(site); + } + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_2.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_2.java new file mode 100644 index 0000000000..8cbe31232d --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_2.java @@ -0,0 +1,901 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mitre.util.JsonUtils.readMap; +import static org.mitre.util.JsonUtils.readSet; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AppType; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SavedUserAuthentication; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mitre.openid.connect.service.MITREidDataServiceExtension; +import org.mitre.openid.connect.service.MITREidDataServiceMaps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWTParser; + +/** + * + * Data service to import and export MITREid 1.2 configuration. + * + * @author jricher + * @author arielak + */ +@Service +@SuppressWarnings(value = {"unchecked"}) +public class MITREidDataService_1_2 extends MITREidDataServiceSupport implements MITREidDataService { + + private static final String DEFAULT_SCOPE = "defaultScope"; + private static final String STRUCTURED_PARAMETER = "structuredParameter"; + private static final String STRUCTURED = "structured"; + private static final String RESTRICTED = "restricted"; + private static final String ICON = "icon"; + private static final String DYNAMICALLY_REGISTERED = "dynamicallyRegistered"; + private static final String CLEAR_ACCESS_TOKENS_ON_REFRESH = "clearAccessTokensOnRefresh"; + private static final String REUSE_REFRESH_TOKEN = "reuseRefreshToken"; + private static final String ALLOW_INTROSPECTION = "allowIntrospection"; + private static final String DESCRIPTION = "description"; + private static final String REQUEST_URIS = "requestUris"; + private static final String POST_LOGOUT_REDIRECT_URI = "postLogoutRedirectUri"; + private static final String INTITATE_LOGIN_URI = "intitateLoginUri"; + private static final String DEFAULT_ACR_VALUES = "defaultACRValues"; + private static final String REQUIRE_AUTH_TIME = "requireAuthTime"; + private static final String DEFAULT_MAX_AGE = "defaultMaxAge"; + private static final String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "tokenEndpointAuthSigningAlg"; + private static final String USER_INFO_ENCRYPTED_RESPONSE_ENC = "userInfoEncryptedResponseEnc"; + private static final String USER_INFO_ENCRYPTED_RESPONSE_ALG = "userInfoEncryptedResponseAlg"; + private static final String USER_INFO_SIGNED_RESPONSE_ALG = "userInfoSignedResponseAlg"; + private static final String ID_TOKEN_ENCRYPTED_RESPONSE_ENC = "idTokenEncryptedResponseEnc"; + private static final String ID_TOKEN_ENCRYPTED_RESPONSE_ALG = "idTokenEncryptedResponseAlg"; + private static final String ID_TOKEN_SIGNED_RESPONSE_ALG = "idTokenSignedResponseAlg"; + private static final String REQUEST_OBJECT_SIGNING_ALG = "requestObjectSigningAlg"; + private static final String SUBJECT_TYPE = "subjectType"; + private static final String SECTOR_IDENTIFIER_URI = "sectorIdentifierUri"; + private static final String APPLICATION_TYPE = "applicationType"; + private static final String JWKS = "jwks"; + private static final String JWKS_URI = "jwksUri"; + private static final String POLICY_URI = "policyUri"; + private static final String GRANT_TYPES = "grantTypes"; + private static final String TOKEN_ENDPOINT_AUTH_METHOD = "tokenEndpointAuthMethod"; + private static final String TOS_URI = "tosUri"; + private static final String CONTACTS = "contacts"; + private static final String LOGO_URI = "logoUri"; + private static final String REDIRECT_URIS = "redirectUris"; + private static final String REFRESH_TOKEN_VALIDITY_SECONDS = "refreshTokenValiditySeconds"; + private static final String ACCESS_TOKEN_VALIDITY_SECONDS = "accessTokenValiditySeconds"; + private static final String SECRET = "secret"; + private static final String URI = "uri"; + private static final String CREATOR_USER_ID = "creatorUserId"; + private static final String APPROVED_ACCESS_TOKENS = "approvedAccessTokens"; + private static final String ALLOWED_SCOPES = "allowedScopes"; + private static final String USER_ID = "userId"; + private static final String TIMEOUT_DATE = "timeoutDate"; + private static final String CREATION_DATE = "creationDate"; + private static final String ACCESS_DATE = "accessDate"; + private static final String AUTHENTICATED = "authenticated"; + private static final String SOURCE_CLASS = "sourceClass"; + private static final String NAME = "name"; + private static final String SAVED_USER_AUTHENTICATION = "savedUserAuthentication"; + private static final String EXTENSIONS = "extensions"; + private static final String RESPONSE_TYPES = "responseTypes"; + private static final String REDIRECT_URI = "redirectUri"; + private static final String APPROVED = "approved"; + private static final String AUTHORITIES = "authorities"; + private static final String RESOURCE_IDS = "resourceIds"; + private static final String REQUEST_PARAMETERS = "requestParameters"; + private static final String TYPE = "type"; + private static final String SCOPE = "scope"; + private static final String REFRESH_TOKEN_ID = "refreshTokenId"; + private static final String VALUE = "value"; + private static final String AUTHENTICATION_HOLDER_ID = "authenticationHolderId"; + private static final String CLIENT_ID = "clientId"; + private static final String EXPIRATION = "expiration"; + private static final String CLAIMS_REDIRECT_URIS = "claimsRedirectUris"; + private static final String ID = "id"; + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(MITREidDataService_1_2.class); + @Autowired + private OAuth2ClientRepository clientRepository; + @Autowired + private ApprovedSiteRepository approvedSiteRepository; + @Autowired + private WhitelistedSiteRepository wlSiteRepository; + @Autowired + private BlacklistedSiteRepository blSiteRepository; + @Autowired + private AuthenticationHolderRepository authHolderRepository; + @Autowired + private OAuth2TokenRepository tokenRepository; + @Autowired + private SystemScopeRepository sysScopeRepository; + @Autowired(required = false) + private List extensions = Collections.emptyList(); + + private MITREidDataServiceMaps maps = new MITREidDataServiceMaps(); + + private static final String THIS_VERSION = MITREID_CONNECT_1_2; + + @Override + public boolean supportsVersion(String version) { + return THIS_VERSION.equals(version); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#export(com.google.gson.stream.JsonWriter) + */ + @Override + public void exportData(JsonWriter writer) throws IOException { + + throw new UnsupportedOperationException("Can not export 1.2 format from this version."); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#importData(com.google.gson.stream.JsonReader) + */ + @Override + public void importData(JsonReader reader) throws IOException { + + logger.info("Reading configuration for 1.2"); + + // this *HAS* to start as an object + reader.beginObject(); + + while (reader.hasNext()) { + JsonToken tok = reader.peek(); + switch (tok) { + case NAME: + String name = reader.nextName(); + // find out which member it is + if (name.equals(CLIENTS)) { + readClients(reader); + } else if (name.equals(GRANTS)) { + readGrants(reader); + } else if (name.equals(WHITELISTEDSITES)) { + readWhitelistedSites(reader); + } else if (name.equals(BLACKLISTEDSITES)) { + readBlacklistedSites(reader); + } else if (name.equals(AUTHENTICATIONHOLDERS)) { + readAuthenticationHolders(reader); + } else if (name.equals(ACCESSTOKENS)) { + readAccessTokens(reader); + } else if (name.equals(REFRESHTOKENS)) { + readRefreshTokens(reader); + } else if (name.equals(SYSTEMSCOPES)) { + readSystemScopes(reader); + } else { + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.importExtensionData(name, reader); + break; + } + } + // unknown token, skip it + reader.skipValue(); + } + break; + case END_OBJECT: + // the object ended, we're done here + reader.endObject(); + continue; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + fixObjectReferences(); + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.fixExtensionObjectReferences(maps); + break; + } + } + maps.clearAll(); + } + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readRefreshTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2RefreshTokenEntity token = new OAuth2RefreshTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(EXPIRATION)) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals(VALUE)) { + String value = reader.nextString(); + try { + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals(CLIENT_ID)) { + clientId = reader.nextString(); + } else if (name.equals(AUTHENTICATION_HOLDER_ID)) { + authHolderId = reader.nextLong(); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveRefreshToken(token).getId(); + maps.getRefreshTokenToClientRefs().put(currentId, clientId); + maps.getRefreshTokenToAuthHolderRefs().put(currentId, authHolderId); + maps.getRefreshTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read refresh token {}", currentId); + } + reader.endArray(); + logger.info("Done reading refresh tokens"); + } + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readAccessTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + Long refreshTokenId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(EXPIRATION)) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals(VALUE)) { + String value = reader.nextString(); + try { + // all tokens are JWTs + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals(CLIENT_ID)) { + clientId = reader.nextString(); + } else if (name.equals(AUTHENTICATION_HOLDER_ID)) { + authHolderId = reader.nextLong(); + } else if (name.equals(REFRESH_TOKEN_ID)) { + refreshTokenId = reader.nextLong(); + } else if (name.equals(SCOPE)) { + Set scope = readSet(reader); + token.setScope(scope); + } else if (name.equals(TYPE)) { + token.setTokenType(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveAccessToken(token).getId(); + maps.getAccessTokenToClientRefs().put(currentId, clientId); + maps.getAccessTokenToAuthHolderRefs().put(currentId, authHolderId); + if (refreshTokenId != null) { + maps.getAccessTokenToRefreshTokenRefs().put(currentId, refreshTokenId); + } + maps.getAccessTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read access token {}", currentId); + } + reader.endArray(); + logger.info("Done reading access tokens"); + } + /** + * @param reader + * @throws IOException + */ + private void readAuthenticationHolders(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + AuthenticationHolderEntity ahe = new AuthenticationHolderEntity(); + reader.beginObject(); + Long currentId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(REQUEST_PARAMETERS)) { + ahe.setRequestParameters(readMap(reader)); + } else if (name.equals(CLIENT_ID)) { + ahe.setClientId(reader.nextString()); + } else if (name.equals(SCOPE)) { + ahe.setScope(readSet(reader)); + } else if (name.equals(RESOURCE_IDS)) { + ahe.setResourceIds(readSet(reader)); + } else if (name.equals(AUTHORITIES)) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + ahe.setAuthorities(authorities); + } else if (name.equals(APPROVED)) { + ahe.setApproved(reader.nextBoolean()); + } else if (name.equals(REDIRECT_URI)) { + ahe.setRedirectUri(reader.nextString()); + } else if (name.equals(RESPONSE_TYPES)) { + ahe.setResponseTypes(readSet(reader)); + } else if (name.equals(EXTENSIONS)) { + ahe.setExtensions(readMap(reader)); + } else if (name.equals(SAVED_USER_AUTHENTICATION)) { + ahe.setUserAuth(readSavedUserAuthentication(reader)); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = authHolderRepository.save(ahe).getId(); + maps.getAuthHolderOldToNewIdMap().put(currentId, newId); + logger.debug("Read authentication holder {}", currentId); + } + reader.endArray(); + logger.info("Done reading authentication holders"); + } + + /** + * @param reader + * @return + * @throws IOException + */ + private SavedUserAuthentication readSavedUserAuthentication(JsonReader reader) throws IOException { + SavedUserAuthentication savedUserAuth = new SavedUserAuthentication(); + reader.beginObject(); + + while (reader.hasNext()) { + switch(reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(NAME)) { + savedUserAuth.setName(reader.nextString()); + } else if (name.equals(SOURCE_CLASS)) { + savedUserAuth.setSourceClass(reader.nextString()); + } else if (name.equals(AUTHENTICATED)) { + savedUserAuth.setAuthenticated(reader.nextBoolean()); + } else if (name.equals(AUTHORITIES)) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + savedUserAuth.setAuthorities(authorities); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + + reader.endObject(); + return savedUserAuth; + } + + /** + * @param reader + * @throws IOException + */ + private void readGrants(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ApprovedSite site = new ApprovedSite(); + Long currentId = null; + Set tokenIds = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(ACCESS_DATE)) { + Date date = utcToDate(reader.nextString()); + site.setAccessDate(date); + } else if (name.equals(CLIENT_ID)) { + site.setClientId(reader.nextString()); + } else if (name.equals(CREATION_DATE)) { + Date date = utcToDate(reader.nextString()); + site.setCreationDate(date); + } else if (name.equals(TIMEOUT_DATE)) { + Date date = utcToDate(reader.nextString()); + site.setTimeoutDate(date); + } else if (name.equals(USER_ID)) { + site.setUserId(reader.nextString()); + } else if (name.equals(ALLOWED_SCOPES)) { + Set allowedScopes = readSet(reader); + site.setAllowedScopes(allowedScopes); + } else if (name.equals(APPROVED_ACCESS_TOKENS)) { + tokenIds = readSet(reader); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = approvedSiteRepository.save(site).getId(); + maps.getGrantOldToNewIdMap().put(currentId, newId); + if (tokenIds != null) { + maps.getGrantToAccessTokensRefs().put(currentId, tokenIds); + } + logger.debug("Read grant {}", currentId); + } + reader.endArray(); + logger.info("Done reading grants"); + } + /** + * @param reader + * @throws IOException + */ + private void readWhitelistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + WhitelistedSite wlSite = new WhitelistedSite(); + Long currentId = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(CLIENT_ID)) { + wlSite.setClientId(reader.nextString()); + } else if (name.equals(CREATOR_USER_ID)) { + wlSite.setCreatorUserId(reader.nextString()); + } else if (name.equals(ALLOWED_SCOPES)) { + Set allowedScopes = readSet(reader); + wlSite.setAllowedScopes(allowedScopes); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = wlSiteRepository.save(wlSite).getId(); + maps.getWhitelistedSiteOldToNewIdMap().put(currentId, newId); + } + reader.endArray(); + logger.info("Done reading whitelisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readBlacklistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + BlacklistedSite blSite = new BlacklistedSite(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals(ID)) { + reader.skipValue(); + } else if (name.equals(URI)) { + blSite.setUri(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + blSiteRepository.save(blSite); + } + reader.endArray(); + logger.info("Done reading blacklisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readClients(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ClientDetailsEntity client = new ClientDetailsEntity(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(CLIENT_ID)) { + client.setClientId(reader.nextString()); + } else if (name.equals(RESOURCE_IDS)) { + Set resourceIds = readSet(reader); + client.setResourceIds(resourceIds); + } else if (name.equals(SECRET)) { + client.setClientSecret(reader.nextString()); + } else if (name.equals(SCOPE)) { + Set scope = readSet(reader); + client.setScope(scope); + } else if (name.equals(AUTHORITIES)) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + client.setAuthorities(authorities); + } else if (name.equals(ACCESS_TOKEN_VALIDITY_SECONDS)) { + client.setAccessTokenValiditySeconds(reader.nextInt()); + } else if (name.equals(REFRESH_TOKEN_VALIDITY_SECONDS)) { + client.setRefreshTokenValiditySeconds(reader.nextInt()); + } else if (name.equals(REDIRECT_URIS)) { + Set redirectUris = readSet(reader); + client.setRedirectUris(redirectUris); + } else if (name.equals(CLAIMS_REDIRECT_URIS)) { + Set claimsRedirectUris = readSet(reader); + client.setClaimsRedirectUris(claimsRedirectUris); + } else if (name.equals(NAME)) { + client.setClientName(reader.nextString()); + } else if (name.equals(URI)) { + client.setClientUri(reader.nextString()); + } else if (name.equals(LOGO_URI)) { + client.setLogoUri(reader.nextString()); + } else if (name.equals(CONTACTS)) { + Set contacts = readSet(reader); + client.setContacts(contacts); + } else if (name.equals(TOS_URI)) { + client.setTosUri(reader.nextString()); + } else if (name.equals(TOKEN_ENDPOINT_AUTH_METHOD)) { + AuthMethod am = AuthMethod.getByValue(reader.nextString()); + client.setTokenEndpointAuthMethod(am); + } else if (name.equals(GRANT_TYPES)) { + Set grantTypes = readSet(reader); + client.setGrantTypes(grantTypes); + } else if (name.equals(RESPONSE_TYPES)) { + Set responseTypes = readSet(reader); + client.setResponseTypes(responseTypes); + } else if (name.equals(POLICY_URI)) { + client.setPolicyUri(reader.nextString()); + } else if (name.equals(APPLICATION_TYPE)) { + AppType appType = AppType.getByValue(reader.nextString()); + client.setApplicationType(appType); + } else if (name.equals(SECTOR_IDENTIFIER_URI)) { + client.setSectorIdentifierUri(reader.nextString()); + } else if (name.equals(SUBJECT_TYPE)) { + SubjectType st = SubjectType.getByValue(reader.nextString()); + client.setSubjectType(st); + } else if (name.equals(JWKS_URI)) { + client.setJwksUri(reader.nextString()); + } else if (name.equals(JWKS)) { + try { + client.setJwks(JWKSet.parse(reader.nextString())); + } catch (ParseException e) { + logger.error("Couldn't parse JWK Set", e); + } + } else if (name.equals(REQUEST_OBJECT_SIGNING_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setRequestObjectSigningAlg(alg); + } else if (name.equals(USER_INFO_ENCRYPTED_RESPONSE_ALG)) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setUserInfoEncryptedResponseAlg(alg); + } else if (name.equals(USER_INFO_ENCRYPTED_RESPONSE_ENC)) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setUserInfoEncryptedResponseEnc(alg); + } else if (name.equals(USER_INFO_SIGNED_RESPONSE_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setUserInfoSignedResponseAlg(alg); + } else if (name.equals(ID_TOKEN_SIGNED_RESPONSE_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setIdTokenSignedResponseAlg(alg); + } else if (name.equals(ID_TOKEN_ENCRYPTED_RESPONSE_ALG)) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setIdTokenEncryptedResponseAlg(alg); + } else if (name.equals(ID_TOKEN_ENCRYPTED_RESPONSE_ENC)) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setIdTokenEncryptedResponseEnc(alg); + } else if (name.equals(TOKEN_ENDPOINT_AUTH_SIGNING_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setTokenEndpointAuthSigningAlg(alg); + } else if (name.equals(DEFAULT_MAX_AGE)) { + client.setDefaultMaxAge(reader.nextInt()); + } else if (name.equals(REQUIRE_AUTH_TIME)) { + client.setRequireAuthTime(reader.nextBoolean()); + } else if (name.equals(DEFAULT_ACR_VALUES)) { + Set defaultACRvalues = readSet(reader); + client.setDefaultACRvalues(defaultACRvalues); + } else if (name.equals("initiateLoginUri")) { + client.setInitiateLoginUri(reader.nextString()); + } else if (name.equals(POST_LOGOUT_REDIRECT_URI)) { + Set postLogoutUris = readSet(reader); + client.setPostLogoutRedirectUris(postLogoutUris); + } else if (name.equals(REQUEST_URIS)) { + Set requestUris = readSet(reader); + client.setRequestUris(requestUris); + } else if (name.equals(DESCRIPTION)) { + client.setClientDescription(reader.nextString()); + } else if (name.equals(ALLOW_INTROSPECTION)) { + client.setAllowIntrospection(reader.nextBoolean()); + } else if (name.equals(REUSE_REFRESH_TOKEN)) { + client.setReuseRefreshToken(reader.nextBoolean()); + } else if (name.equals(CLEAR_ACCESS_TOKENS_ON_REFRESH)) { + client.setClearAccessTokensOnRefresh(reader.nextBoolean()); + } else if (name.equals(DYNAMICALLY_REGISTERED)) { + client.setDynamicallyRegistered(reader.nextBoolean()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + clientRepository.saveClient(client); + } + reader.endArray(); + logger.info("Done reading clients"); + } + + /** + * Read the list of system scopes from the reader and insert them into the + * scope repository. + * + * @param reader + * @throws IOException + */ + private void readSystemScopes(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + SystemScope scope = new SystemScope(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(VALUE)) { + scope.setValue(reader.nextString()); + } else if (name.equals(DESCRIPTION)) { + scope.setDescription(reader.nextString()); + } else if (name.equals(RESTRICTED)) { + scope.setRestricted(reader.nextBoolean()); + } else if (name.equals(DEFAULT_SCOPE)) { + scope.setDefaultScope(reader.nextBoolean()); + } else if (name.equals(ICON)) { + scope.setIcon(reader.nextString()); + } else if (name.equals(STRUCTURED)) { + logger.warn("Found a structured scope, ignoring structure"); + } else if (name.equals(STRUCTURED_PARAMETER)) { + logger.warn("Found a structured scope, ignoring structure"); + } else { + logger.debug("found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + sysScopeRepository.save(scope); + } + reader.endArray(); + logger.info("Done reading system scopes"); + } + + private void fixObjectReferences() { + logger.info("Fixing object references..."); + for (Long oldRefreshTokenId : maps.getRefreshTokenToClientRefs().keySet()) { + String clientRef = maps.getRefreshTokenToClientRefs().get(oldRefreshTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setClient(client); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldRefreshTokenId : maps.getRefreshTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getRefreshTokenToAuthHolderRefs().get(oldRefreshTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setAuthenticationHolder(authHolder); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToClientRefs().keySet()) { + String clientRef = maps.getAccessTokenToClientRefs().get(oldAccessTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setClient(client); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getAccessTokenToAuthHolderRefs().get(oldAccessTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setAuthenticationHolder(authHolder); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToRefreshTokenRefs().keySet()) { + Long oldRefreshTokenId = maps.getAccessTokenToRefreshTokenRefs().get(oldAccessTokenId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setRefreshToken(refreshToken); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldGrantId : maps.getGrantToAccessTokensRefs().keySet()) { + Set oldAccessTokenIds = maps.getGrantToAccessTokensRefs().get(oldGrantId); + + Long newGrantId = maps.getGrantOldToNewIdMap().get(oldGrantId); + ApprovedSite site = approvedSiteRepository.getById(newGrantId); + + for(Long oldTokenId : oldAccessTokenIds) { + Long newTokenId = maps.getAccessTokenOldToNewIdMap().get(oldTokenId); + OAuth2AccessTokenEntity token = tokenRepository.getAccessTokenById(newTokenId); + token.setApprovedSite(site); + tokenRepository.saveAccessToken(token); + } + + approvedSiteRepository.save(site); + } + logger.info("Done fixing object references."); + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_3.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_3.java new file mode 100644 index 0000000000..2377306e57 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_3.java @@ -0,0 +1,1314 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mitre.util.JsonUtils.readMap; +import static org.mitre.util.JsonUtils.readSet; +import static org.mitre.util.JsonUtils.writeNullSafeArray; + +import java.io.IOException; +import java.io.Serializable; +import java.text.ParseException; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AppType; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.PKCEAlgorithm; +import org.mitre.oauth2.model.SavedUserAuthentication; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mitre.openid.connect.service.MITREidDataServiceExtension; +import org.mitre.openid.connect.service.MITREidDataServiceMaps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWTParser; + +/** + * + * Data service to import and export MITREid 1.3 configuration. + * + * @author jricher + * @author arielak + */ +@Service +@SuppressWarnings(value = {"unchecked"}) +public class MITREidDataService_1_3 extends MITREidDataServiceSupport implements MITREidDataService { + + private static final String DEFAULT_SCOPE = "defaultScope"; + private static final String RESTRICTED = "restricted"; + private static final String ICON = "icon"; + private static final String DYNAMICALLY_REGISTERED = "dynamicallyRegistered"; + private static final String CLEAR_ACCESS_TOKENS_ON_REFRESH = "clearAccessTokensOnRefresh"; + private static final String REUSE_REFRESH_TOKEN = "reuseRefreshToken"; + private static final String ALLOW_INTROSPECTION = "allowIntrospection"; + private static final String DESCRIPTION = "description"; + private static final String REQUEST_URIS = "requestUris"; + private static final String POST_LOGOUT_REDIRECT_URI = "postLogoutRedirectUri"; + private static final String INTITATE_LOGIN_URI = "intitateLoginUri"; + private static final String DEFAULT_ACR_VALUES = "defaultACRValues"; + private static final String REQUIRE_AUTH_TIME = "requireAuthTime"; + private static final String DEFAULT_MAX_AGE = "defaultMaxAge"; + private static final String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "tokenEndpointAuthSigningAlg"; + private static final String USER_INFO_ENCRYPTED_RESPONSE_ENC = "userInfoEncryptedResponseEnc"; + private static final String USER_INFO_ENCRYPTED_RESPONSE_ALG = "userInfoEncryptedResponseAlg"; + private static final String USER_INFO_SIGNED_RESPONSE_ALG = "userInfoSignedResponseAlg"; + private static final String ID_TOKEN_ENCRYPTED_RESPONSE_ENC = "idTokenEncryptedResponseEnc"; + private static final String ID_TOKEN_ENCRYPTED_RESPONSE_ALG = "idTokenEncryptedResponseAlg"; + private static final String ID_TOKEN_SIGNED_RESPONSE_ALG = "idTokenSignedResponseAlg"; + private static final String REQUEST_OBJECT_SIGNING_ALG = "requestObjectSigningAlg"; + private static final String SUBJECT_TYPE = "subjectType"; + private static final String SECTOR_IDENTIFIER_URI = "sectorIdentifierUri"; + private static final String APPLICATION_TYPE = "applicationType"; + private static final String JWKS = "jwks"; + private static final String JWKS_URI = "jwksUri"; + private static final String POLICY_URI = "policyUri"; + private static final String GRANT_TYPES = "grantTypes"; + private static final String TOKEN_ENDPOINT_AUTH_METHOD = "tokenEndpointAuthMethod"; + private static final String TOS_URI = "tosUri"; + private static final String CONTACTS = "contacts"; + private static final String LOGO_URI = "logoUri"; + private static final String REDIRECT_URIS = "redirectUris"; + private static final String REFRESH_TOKEN_VALIDITY_SECONDS = "refreshTokenValiditySeconds"; + private static final String ACCESS_TOKEN_VALIDITY_SECONDS = "accessTokenValiditySeconds"; + private static final String ID_TOKEN_VALIDITY_SECONDS = "idTokenValiditySeconds"; + private static final String DEVICE_CODE_VALIDITY_SECONDS = "deviceCodeValiditySeconds"; + private static final String SECRET = "secret"; + private static final String URI = "uri"; + private static final String CREATOR_USER_ID = "creatorUserId"; + private static final String APPROVED_ACCESS_TOKENS = "approvedAccessTokens"; + private static final String ALLOWED_SCOPES = "allowedScopes"; + private static final String USER_ID = "userId"; + private static final String TIMEOUT_DATE = "timeoutDate"; + private static final String CREATION_DATE = "creationDate"; + private static final String ACCESS_DATE = "accessDate"; + private static final String AUTHENTICATED = "authenticated"; + private static final String SOURCE_CLASS = "sourceClass"; + private static final String NAME = "name"; + private static final String SAVED_USER_AUTHENTICATION = "savedUserAuthentication"; + private static final String EXTENSIONS = "extensions"; + private static final String RESPONSE_TYPES = "responseTypes"; + private static final String REDIRECT_URI = "redirectUri"; + private static final String APPROVED = "approved"; + private static final String AUTHORITIES = "authorities"; + private static final String RESOURCE_IDS = "resourceIds"; + private static final String REQUEST_PARAMETERS = "requestParameters"; + private static final String TYPE = "type"; + private static final String SCOPE = "scope"; + private static final String REFRESH_TOKEN_ID = "refreshTokenId"; + private static final String VALUE = "value"; + private static final String AUTHENTICATION_HOLDER_ID = "authenticationHolderId"; + private static final String CLIENT_ID = "clientId"; + private static final String EXPIRATION = "expiration"; + private static final String CLAIMS_REDIRECT_URIS = "claimsRedirectUris"; + private static final String ID = "id"; + private static final String CODE_CHALLENGE_METHOD = "codeChallengeMethod"; + private static final String SOFTWARE_STATEMENT = "softwareStatement"; + private static final String SOFTWARE_VERSION = "softwareVersion"; + private static final String SOFTWARE_ID = "softwareId"; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(MITREidDataService_1_3.class); + @Autowired + private OAuth2ClientRepository clientRepository; + @Autowired + private ApprovedSiteRepository approvedSiteRepository; + @Autowired + private WhitelistedSiteRepository wlSiteRepository; + @Autowired + private BlacklistedSiteRepository blSiteRepository; + @Autowired + private AuthenticationHolderRepository authHolderRepository; + @Autowired + private OAuth2TokenRepository tokenRepository; + @Autowired + private SystemScopeRepository sysScopeRepository; + @Autowired(required = false) + private List extensions = Collections.emptyList(); + + private static final String THIS_VERSION = MITREID_CONNECT_1_3; + + private MITREidDataServiceMaps maps = new MITREidDataServiceMaps(); + + @Override + public boolean supportsVersion(String version) { + return THIS_VERSION.equals(version); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#export(com.google.gson.stream.JsonWriter) + */ + @Override + public void exportData(JsonWriter writer) throws IOException { + + // version tag at the root + writer.name(THIS_VERSION); + + writer.beginObject(); + + // clients list + writer.name(CLIENTS); + writer.beginArray(); + writeClients(writer); + writer.endArray(); + + writer.name(GRANTS); + writer.beginArray(); + writeGrants(writer); + writer.endArray(); + + writer.name(WHITELISTEDSITES); + writer.beginArray(); + writeWhitelistedSites(writer); + writer.endArray(); + + writer.name(BLACKLISTEDSITES); + writer.beginArray(); + writeBlacklistedSites(writer); + writer.endArray(); + + writer.name(AUTHENTICATIONHOLDERS); + writer.beginArray(); + writeAuthenticationHolders(writer); + writer.endArray(); + + writer.name(ACCESSTOKENS); + writer.beginArray(); + writeAccessTokens(writer); + writer.endArray(); + + writer.name(REFRESHTOKENS); + writer.beginArray(); + writeRefreshTokens(writer); + writer.endArray(); + + writer.name(SYSTEMSCOPES); + writer.beginArray(); + writeSystemScopes(writer); + writer.endArray(); + + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.exportExtensionData(writer); + break; + } + } + + writer.endObject(); // end mitreid-connect-1.3 + } + + /** + * @param writer + */ + private void writeRefreshTokens(JsonWriter writer) throws IOException { + for (OAuth2RefreshTokenEntity token : tokenRepository.getAllRefreshTokens()) { + writer.beginObject(); + writer.name(ID).value(token.getId()); + writer.name(EXPIRATION).value(toUTCString(token.getExpiration())); + writer.name(CLIENT_ID) + .value((token.getClient() != null) ? token.getClient().getClientId() : null); + writer.name(AUTHENTICATION_HOLDER_ID) + .value((token.getAuthenticationHolder() != null) ? token.getAuthenticationHolder().getId() : null); + writer.name(VALUE).value(token.getValue()); + writer.endObject(); + logger.debug("Wrote refresh token {}", token.getId()); + } + logger.info("Done writing refresh tokens"); + } + + /** + * @param writer + */ + private void writeAccessTokens(JsonWriter writer) throws IOException { + for (OAuth2AccessTokenEntity token : tokenRepository.getAllAccessTokens()) { + writer.beginObject(); + writer.name(ID).value(token.getId()); + writer.name(EXPIRATION).value(toUTCString(token.getExpiration())); + writer.name(CLIENT_ID) + .value((token.getClient() != null) ? token.getClient().getClientId() : null); + writer.name(AUTHENTICATION_HOLDER_ID) + .value((token.getAuthenticationHolder() != null) ? token.getAuthenticationHolder().getId() : null); + writer.name(REFRESH_TOKEN_ID) + .value((token.getRefreshToken() != null) ? token.getRefreshToken().getId() : null); + writer.name(SCOPE); + writer.beginArray(); + for (String s : token.getScope()) { + writer.value(s); + } + writer.endArray(); + writer.name(TYPE).value(token.getTokenType()); + writer.name(VALUE).value(token.getValue()); + writer.endObject(); + logger.debug("Wrote access token {}", token.getId()); + } + logger.info("Done writing access tokens"); + } + + /** + * @param writer + */ + private void writeAuthenticationHolders(JsonWriter writer) throws IOException { + for (AuthenticationHolderEntity holder : authHolderRepository.getAll()) { + writer.beginObject(); + writer.name(ID).value(holder.getId()); + + writer.name(REQUEST_PARAMETERS); + writer.beginObject(); + for (Entry entry : holder.getRequestParameters().entrySet()) { + writer.name(entry.getKey()).value(entry.getValue()); + } + writer.endObject(); + writer.name(CLIENT_ID).value(holder.getClientId()); + Set scope = holder.getScope(); + writer.name(SCOPE); + writer.beginArray(); + for (String s : scope) { + writer.value(s); + } + writer.endArray(); + writer.name(RESOURCE_IDS); + writer.beginArray(); + if (holder.getResourceIds() != null) { + for (String s : holder.getResourceIds()) { + writer.value(s); + } + } + writer.endArray(); + writer.name(AUTHORITIES); + writer.beginArray(); + for (GrantedAuthority authority : holder.getAuthorities()) { + writer.value(authority.getAuthority()); + } + writer.endArray(); + writer.name(APPROVED).value(holder.isApproved()); + writer.name(REDIRECT_URI).value(holder.getRedirectUri()); + writer.name(RESPONSE_TYPES); + writer.beginArray(); + for (String s : holder.getResponseTypes()) { + writer.value(s); + } + writer.endArray(); + writer.name(EXTENSIONS); + writer.beginObject(); + for (Entry entry : holder.getExtensions().entrySet()) { + // while the extension map itself is Serializable, we enforce storage of Strings + if (entry.getValue() instanceof String) { + writer.name(entry.getKey()).value((String) entry.getValue()); + } else { + logger.warn("Skipping non-string extension: " + entry); + } + } + writer.endObject(); + + writer.name(SAVED_USER_AUTHENTICATION); + if (holder.getUserAuth() != null) { + writer.beginObject(); + writer.name(NAME).value(holder.getUserAuth().getName()); + writer.name(SOURCE_CLASS).value(holder.getUserAuth().getSourceClass()); + writer.name(AUTHENTICATED).value(holder.getUserAuth().isAuthenticated()); + writer.name(AUTHORITIES); + writer.beginArray(); + for (GrantedAuthority authority : holder.getUserAuth().getAuthorities()) { + writer.value(authority.getAuthority()); + } + writer.endArray(); + + writer.endObject(); + } else { + writer.nullValue(); + } + + + writer.endObject(); + logger.debug("Wrote authentication holder {}", holder.getId()); + } + logger.info("Done writing authentication holders"); + } + + /** + * @param writer + */ + private void writeGrants(JsonWriter writer) throws IOException { + for (ApprovedSite site : approvedSiteRepository.getAll()) { + writer.beginObject(); + writer.name(ID).value(site.getId()); + writer.name(ACCESS_DATE).value(toUTCString(site.getAccessDate())); + writer.name(CLIENT_ID).value(site.getClientId()); + writer.name(CREATION_DATE).value(toUTCString(site.getCreationDate())); + writer.name(TIMEOUT_DATE).value(toUTCString(site.getTimeoutDate())); + writer.name(USER_ID).value(site.getUserId()); + writer.name(ALLOWED_SCOPES); + writeNullSafeArray(writer, site.getAllowedScopes()); + List tokens = tokenRepository.getAccessTokensForApprovedSite(site); + writer.name(APPROVED_ACCESS_TOKENS); + writer.beginArray(); + for (OAuth2AccessTokenEntity token : tokens) { + writer.value(token.getId()); + } + writer.endArray(); + writer.endObject(); + logger.debug("Wrote grant {}", site.getId()); + } + logger.info("Done writing grants"); + } + + /** + * @param writer + */ + private void writeWhitelistedSites(JsonWriter writer) throws IOException { + for (WhitelistedSite wlSite : wlSiteRepository.getAll()) { + writer.beginObject(); + writer.name(ID).value(wlSite.getId()); + writer.name(CLIENT_ID).value(wlSite.getClientId()); + writer.name(CREATOR_USER_ID).value(wlSite.getCreatorUserId()); + writer.name(ALLOWED_SCOPES); + writeNullSafeArray(writer, wlSite.getAllowedScopes()); + writer.endObject(); + logger.debug("Wrote whitelisted site {}", wlSite.getId()); + } + logger.info("Done writing whitelisted sites"); + } + + /** + * @param writer + */ + private void writeBlacklistedSites(JsonWriter writer) throws IOException { + for (BlacklistedSite blSite : blSiteRepository.getAll()) { + writer.beginObject(); + writer.name(ID).value(blSite.getId()); + writer.name(URI).value(blSite.getUri()); + writer.endObject(); + logger.debug("Wrote blacklisted site {}", blSite.getId()); + } + logger.info("Done writing blacklisted sites"); + } + + /** + * @param writer + */ + private void writeClients(JsonWriter writer) { + for (ClientDetailsEntity client : clientRepository.getAllClients()) { + try { + writer.beginObject(); + writer.name(CLIENT_ID).value(client.getClientId()); + writer.name(RESOURCE_IDS); + writeNullSafeArray(writer, client.getResourceIds()); + + writer.name(SECRET).value(client.getClientSecret()); + + writer.name(SCOPE); + writeNullSafeArray(writer, client.getScope()); + + writer.name(AUTHORITIES); + writer.beginArray(); + for (GrantedAuthority authority : client.getAuthorities()) { + writer.value(authority.getAuthority()); + } + writer.endArray(); + writer.name(ACCESS_TOKEN_VALIDITY_SECONDS).value(client.getAccessTokenValiditySeconds()); + writer.name(REFRESH_TOKEN_VALIDITY_SECONDS).value(client.getRefreshTokenValiditySeconds()); + writer.name(ID_TOKEN_VALIDITY_SECONDS).value(client.getIdTokenValiditySeconds()); + writer.name(DEVICE_CODE_VALIDITY_SECONDS).value(client.getDeviceCodeValiditySeconds()); + writer.name(REDIRECT_URIS); + writeNullSafeArray(writer, client.getRedirectUris()); + writer.name(CLAIMS_REDIRECT_URIS); + writeNullSafeArray(writer, client.getClaimsRedirectUris()); + writer.name(NAME).value(client.getClientName()); + writer.name(URI).value(client.getClientUri()); + writer.name(LOGO_URI).value(client.getLogoUri()); + writer.name(CONTACTS); + writeNullSafeArray(writer, client.getContacts()); + writer.name(TOS_URI).value(client.getTosUri()); + writer.name(TOKEN_ENDPOINT_AUTH_METHOD) + .value((client.getTokenEndpointAuthMethod() != null) ? client.getTokenEndpointAuthMethod().getValue() : null); + writer.name(GRANT_TYPES); + writer.beginArray(); + for (String s : client.getGrantTypes()) { + writer.value(s); + } + writer.endArray(); + writer.name(RESPONSE_TYPES); + writer.beginArray(); + for (String s : client.getResponseTypes()) { + writer.value(s); + } + writer.endArray(); + writer.name(POLICY_URI).value(client.getPolicyUri()); + writer.name(JWKS_URI).value(client.getJwksUri()); + writer.name(JWKS).value((client.getJwks() != null) ? client.getJwks().toString() : null); + writer.name(APPLICATION_TYPE) + .value((client.getApplicationType() != null) ? client.getApplicationType().getValue() : null); + writer.name(SECTOR_IDENTIFIER_URI).value(client.getSectorIdentifierUri()); + writer.name(SUBJECT_TYPE) + .value((client.getSubjectType() != null) ? client.getSubjectType().getValue() : null); + writer.name(REQUEST_OBJECT_SIGNING_ALG) + .value((client.getRequestObjectSigningAlg() != null) ? client.getRequestObjectSigningAlg().getName() : null); + writer.name(ID_TOKEN_SIGNED_RESPONSE_ALG) + .value((client.getIdTokenSignedResponseAlg() != null) ? client.getIdTokenSignedResponseAlg().getName() : null); + writer.name(ID_TOKEN_ENCRYPTED_RESPONSE_ALG) + .value((client.getIdTokenEncryptedResponseAlg() != null) ? client.getIdTokenEncryptedResponseAlg().getName() : null); + writer.name(ID_TOKEN_ENCRYPTED_RESPONSE_ENC) + .value((client.getIdTokenEncryptedResponseEnc() != null) ? client.getIdTokenEncryptedResponseEnc().getName() : null); + writer.name(USER_INFO_SIGNED_RESPONSE_ALG) + .value((client.getUserInfoSignedResponseAlg() != null) ? client.getUserInfoSignedResponseAlg().getName() : null); + writer.name(USER_INFO_ENCRYPTED_RESPONSE_ALG) + .value((client.getUserInfoEncryptedResponseAlg() != null) ? client.getUserInfoEncryptedResponseAlg().getName() : null); + writer.name(USER_INFO_ENCRYPTED_RESPONSE_ENC) + .value((client.getUserInfoEncryptedResponseEnc() != null) ? client.getUserInfoEncryptedResponseEnc().getName() : null); + writer.name(TOKEN_ENDPOINT_AUTH_SIGNING_ALG) + .value((client.getTokenEndpointAuthSigningAlg() != null) ? client.getTokenEndpointAuthSigningAlg().getName() : null); + writer.name(DEFAULT_MAX_AGE).value(client.getDefaultMaxAge()); + Boolean requireAuthTime = null; + try { + requireAuthTime = client.getRequireAuthTime(); + } catch (NullPointerException e) { + } + if (requireAuthTime != null) { + writer.name(REQUIRE_AUTH_TIME).value(requireAuthTime); + } + writer.name(DEFAULT_ACR_VALUES); + writeNullSafeArray(writer, client.getDefaultACRvalues()); + writer.name(INTITATE_LOGIN_URI).value(client.getInitiateLoginUri()); + writer.name(POST_LOGOUT_REDIRECT_URI); + writeNullSafeArray(writer, client.getPostLogoutRedirectUris()); + writer.name(REQUEST_URIS); + writeNullSafeArray(writer, client.getRequestUris()); + writer.name(DESCRIPTION).value(client.getClientDescription()); + writer.name(ALLOW_INTROSPECTION).value(client.isAllowIntrospection()); + writer.name(REUSE_REFRESH_TOKEN).value(client.isReuseRefreshToken()); + writer.name(CLEAR_ACCESS_TOKENS_ON_REFRESH).value(client.isClearAccessTokensOnRefresh()); + writer.name(DYNAMICALLY_REGISTERED).value(client.isDynamicallyRegistered()); + writer.name(CODE_CHALLENGE_METHOD).value(client.getCodeChallengeMethod() != null ? client.getCodeChallengeMethod().getName() : null); + writer.name(SOFTWARE_ID).value(client.getSoftwareId()); + writer.name(SOFTWARE_VERSION).value(client.getSoftwareVersion()); + writer.name(SOFTWARE_STATEMENT).value(client.getSoftwareStatement() != null ? client.getSoftwareStatement().serialize() : null); + writer.name(CREATION_DATE).value(toUTCString(client.getCreatedAt())); + writer.endObject(); + logger.debug("Wrote client {}", client.getId()); + } catch (IOException ex) { + logger.error("Unable to write client {}", client.getId(), ex); + } + } + logger.info("Done writing clients"); + } + + /** + * @param writer + */ + private void writeSystemScopes(JsonWriter writer) { + for (SystemScope sysScope : sysScopeRepository.getAll()) { + try { + writer.beginObject(); + writer.name(ID).value(sysScope.getId()); + writer.name(DESCRIPTION).value(sysScope.getDescription()); + writer.name(ICON).value(sysScope.getIcon()); + writer.name(VALUE).value(sysScope.getValue()); + writer.name(RESTRICTED).value(sysScope.isRestricted()); + writer.name(DEFAULT_SCOPE).value(sysScope.isDefaultScope()); + writer.endObject(); + logger.debug("Wrote system scope {}", sysScope.getId()); + } catch (IOException ex) { + logger.error("Unable to write system scope {}", sysScope.getId(), ex); + } + } + logger.info("Done writing system scopes"); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataService#importData(com.google.gson.stream.JsonReader) + */ + @Override + public void importData(JsonReader reader) throws IOException { + + logger.info("Reading configuration for 1.3"); + + // this *HAS* to start as an object + reader.beginObject(); + + while (reader.hasNext()) { + JsonToken tok = reader.peek(); + switch (tok) { + case NAME: + String name = reader.nextName(); + // find out which member it is + if (name.equals(CLIENTS)) { + readClients(reader); + } else if (name.equals(GRANTS)) { + readGrants(reader); + } else if (name.equals(WHITELISTEDSITES)) { + readWhitelistedSites(reader); + } else if (name.equals(BLACKLISTEDSITES)) { + readBlacklistedSites(reader); + } else if (name.equals(AUTHENTICATIONHOLDERS)) { + readAuthenticationHolders(reader); + } else if (name.equals(ACCESSTOKENS)) { + readAccessTokens(reader); + } else if (name.equals(REFRESHTOKENS)) { + readRefreshTokens(reader); + } else if (name.equals(SYSTEMSCOPES)) { + readSystemScopes(reader); + } else { + boolean processed = false; + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + processed = extension.importExtensionData(name, reader); + if (processed) { + // if the extension processed data, break out of this inner loop + // (only the first extension to claim an extension point gets it) + break; + } + } + } + if (!processed) { + // unknown token, skip it + reader.skipValue(); + } + } + break; + case END_OBJECT: + // the object ended, we're done here + reader.endObject(); + continue; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + fixObjectReferences(); + for (MITREidDataServiceExtension extension : extensions) { + if (extension.supportsVersion(THIS_VERSION)) { + extension.fixExtensionObjectReferences(maps); + break; + } + } + maps.clearAll(); + } + + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readRefreshTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2RefreshTokenEntity token = new OAuth2RefreshTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(EXPIRATION)) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals(VALUE)) { + String value = reader.nextString(); + try { + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals(CLIENT_ID)) { + clientId = reader.nextString(); + } else if (name.equals(AUTHENTICATION_HOLDER_ID)) { + authHolderId = reader.nextLong(); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveRefreshToken(token).getId(); + maps.getRefreshTokenToClientRefs().put(currentId, clientId); + maps.getRefreshTokenToAuthHolderRefs().put(currentId, authHolderId); + maps.getRefreshTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read refresh token {}", currentId); + } + reader.endArray(); + logger.info("Done reading refresh tokens"); + } + /** + * @param reader + * @throws IOException + */ + /** + * @param reader + * @throws IOException + */ + private void readAccessTokens(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + reader.beginObject(); + Long currentId = null; + String clientId = null; + Long authHolderId = null; + Long refreshTokenId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(EXPIRATION)) { + Date date = utcToDate(reader.nextString()); + token.setExpiration(date); + } else if (name.equals(VALUE)) { + String value = reader.nextString(); + try { + // all tokens are JWTs + token.setJwt(JWTParser.parse(value)); + } catch (ParseException ex) { + logger.error("Unable to set refresh token value to {}", value, ex); + } + } else if (name.equals(CLIENT_ID)) { + clientId = reader.nextString(); + } else if (name.equals(AUTHENTICATION_HOLDER_ID)) { + authHolderId = reader.nextLong(); + } else if (name.equals(REFRESH_TOKEN_ID)) { + refreshTokenId = reader.nextLong(); + } else if (name.equals(SCOPE)) { + Set scope = readSet(reader); + token.setScope(scope); + } else if (name.equals(TYPE)) { + token.setTokenType(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = tokenRepository.saveAccessToken(token).getId(); + maps.getAccessTokenToClientRefs().put(currentId, clientId); + maps.getAccessTokenToAuthHolderRefs().put(currentId, authHolderId); + if (refreshTokenId != null) { + maps.getAccessTokenToRefreshTokenRefs().put(currentId, refreshTokenId); + } + maps.getAccessTokenOldToNewIdMap().put(currentId, newId); + logger.debug("Read access token {}", currentId); + } + reader.endArray(); + logger.info("Done reading access tokens"); + } + /** + * @param reader + * @throws IOException + */ + private void readAuthenticationHolders(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + AuthenticationHolderEntity ahe = new AuthenticationHolderEntity(); + reader.beginObject(); + Long currentId = null; + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(REQUEST_PARAMETERS)) { + ahe.setRequestParameters(readMap(reader)); + } else if (name.equals(CLIENT_ID)) { + ahe.setClientId(reader.nextString()); + } else if (name.equals(SCOPE)) { + ahe.setScope(readSet(reader)); + } else if (name.equals(RESOURCE_IDS)) { + ahe.setResourceIds(readSet(reader)); + } else if (name.equals(AUTHORITIES)) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + ahe.setAuthorities(authorities); + } else if (name.equals(APPROVED)) { + ahe.setApproved(reader.nextBoolean()); + } else if (name.equals(REDIRECT_URI)) { + ahe.setRedirectUri(reader.nextString()); + } else if (name.equals(RESPONSE_TYPES)) { + ahe.setResponseTypes(readSet(reader)); + } else if (name.equals(EXTENSIONS)) { + ahe.setExtensions(readMap(reader)); + } else if (name.equals(SAVED_USER_AUTHENTICATION)) { + ahe.setUserAuth(readSavedUserAuthentication(reader)); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = authHolderRepository.save(ahe).getId(); + maps.getAuthHolderOldToNewIdMap().put(currentId, newId); + logger.debug("Read authentication holder {}", currentId); + } + reader.endArray(); + logger.info("Done reading authentication holders"); + } + + /** + * @param reader + * @return + * @throws IOException + */ + private SavedUserAuthentication readSavedUserAuthentication(JsonReader reader) throws IOException { + SavedUserAuthentication savedUserAuth = new SavedUserAuthentication(); + reader.beginObject(); + + while (reader.hasNext()) { + switch(reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(NAME)) { + savedUserAuth.setName(reader.nextString()); + } else if (name.equals(SOURCE_CLASS)) { + savedUserAuth.setSourceClass(reader.nextString()); + } else if (name.equals(AUTHENTICATED)) { + savedUserAuth.setAuthenticated(reader.nextBoolean()); + } else if (name.equals(AUTHORITIES)) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + savedUserAuth.setAuthorities(authorities); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + + reader.endObject(); + return savedUserAuth; + } + + /** + * @param reader + * @throws IOException + */ + private void readGrants(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ApprovedSite site = new ApprovedSite(); + Long currentId = null; + Set tokenIds = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(ACCESS_DATE)) { + Date date = utcToDate(reader.nextString()); + site.setAccessDate(date); + } else if (name.equals(CLIENT_ID)) { + site.setClientId(reader.nextString()); + } else if (name.equals(CREATION_DATE)) { + Date date = utcToDate(reader.nextString()); + site.setCreationDate(date); + } else if (name.equals(TIMEOUT_DATE)) { + Date date = utcToDate(reader.nextString()); + site.setTimeoutDate(date); + } else if (name.equals(USER_ID)) { + site.setUserId(reader.nextString()); + } else if (name.equals(ALLOWED_SCOPES)) { + Set allowedScopes = readSet(reader); + site.setAllowedScopes(allowedScopes); + } else if (name.equals(APPROVED_ACCESS_TOKENS)) { + tokenIds = readSet(reader); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = approvedSiteRepository.save(site).getId(); + maps.getGrantOldToNewIdMap().put(currentId, newId); + if (tokenIds != null) { + maps.getGrantToAccessTokensRefs().put(currentId, tokenIds); + } + logger.debug("Read grant {}", currentId); + } + reader.endArray(); + logger.info("Done reading grants"); + } + + /** + * @param reader + * @throws IOException + */ + private void readWhitelistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + WhitelistedSite wlSite = new WhitelistedSite(); + Long currentId = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals(ID)) { + currentId = reader.nextLong(); + } else if (name.equals(CLIENT_ID)) { + wlSite.setClientId(reader.nextString()); + } else if (name.equals(CREATOR_USER_ID)) { + wlSite.setCreatorUserId(reader.nextString()); + } else if (name.equals(ALLOWED_SCOPES)) { + Set allowedScopes = readSet(reader); + wlSite.setAllowedScopes(allowedScopes); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = wlSiteRepository.save(wlSite).getId(); + maps.getWhitelistedSiteOldToNewIdMap().put(currentId, newId); + } + reader.endArray(); + logger.info("Done reading whitelisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readBlacklistedSites(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + BlacklistedSite blSite = new BlacklistedSite(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals(ID)) { + reader.skipValue(); + } else if (name.equals(URI)) { + blSite.setUri(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + blSiteRepository.save(blSite); + } + reader.endArray(); + logger.info("Done reading blacklisted sites"); + } + + /** + * @param reader + * @throws IOException + */ + private void readClients(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + ClientDetailsEntity client = new ClientDetailsEntity(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(CLIENT_ID)) { + client.setClientId(reader.nextString()); + } else if (name.equals(RESOURCE_IDS)) { + Set resourceIds = readSet(reader); + client.setResourceIds(resourceIds); + } else if (name.equals(SECRET)) { + client.setClientSecret(reader.nextString()); + } else if (name.equals(SCOPE)) { + Set scope = readSet(reader); + client.setScope(scope); + } else if (name.equals(AUTHORITIES)) { + Set authorityStrs = readSet(reader); + Set authorities = new HashSet(); + for (String s : authorityStrs) { + GrantedAuthority ga = new SimpleGrantedAuthority(s); + authorities.add(ga); + } + client.setAuthorities(authorities); + } else if (name.equals(ACCESS_TOKEN_VALIDITY_SECONDS)) { + client.setAccessTokenValiditySeconds(reader.nextInt()); + } else if (name.equals(REFRESH_TOKEN_VALIDITY_SECONDS)) { + client.setRefreshTokenValiditySeconds(reader.nextInt()); + } else if (name.equals(ID_TOKEN_VALIDITY_SECONDS)) { + client.setIdTokenValiditySeconds(reader.nextInt()); + } else if (name.equals(DEVICE_CODE_VALIDITY_SECONDS)) { + client.setDeviceCodeValiditySeconds(reader.nextInt()); + } else if (name.equals(REDIRECT_URIS)) { + Set redirectUris = readSet(reader); + client.setRedirectUris(redirectUris); + } else if (name.equals(CLAIMS_REDIRECT_URIS)) { + Set claimsRedirectUris = readSet(reader); + client.setClaimsRedirectUris(claimsRedirectUris); + } else if (name.equals(NAME)) { + client.setClientName(reader.nextString()); + } else if (name.equals(URI)) { + client.setClientUri(reader.nextString()); + } else if (name.equals(LOGO_URI)) { + client.setLogoUri(reader.nextString()); + } else if (name.equals(CONTACTS)) { + Set contacts = readSet(reader); + client.setContacts(contacts); + } else if (name.equals(TOS_URI)) { + client.setTosUri(reader.nextString()); + } else if (name.equals(TOKEN_ENDPOINT_AUTH_METHOD)) { + AuthMethod am = AuthMethod.getByValue(reader.nextString()); + client.setTokenEndpointAuthMethod(am); + } else if (name.equals(GRANT_TYPES)) { + Set grantTypes = readSet(reader); + client.setGrantTypes(grantTypes); + } else if (name.equals(RESPONSE_TYPES)) { + Set responseTypes = readSet(reader); + client.setResponseTypes(responseTypes); + } else if (name.equals(POLICY_URI)) { + client.setPolicyUri(reader.nextString()); + } else if (name.equals(APPLICATION_TYPE)) { + AppType appType = AppType.getByValue(reader.nextString()); + client.setApplicationType(appType); + } else if (name.equals(SECTOR_IDENTIFIER_URI)) { + client.setSectorIdentifierUri(reader.nextString()); + } else if (name.equals(SUBJECT_TYPE)) { + SubjectType st = SubjectType.getByValue(reader.nextString()); + client.setSubjectType(st); + } else if (name.equals(JWKS_URI)) { + client.setJwksUri(reader.nextString()); + } else if (name.equals(JWKS)) { + try { + client.setJwks(JWKSet.parse(reader.nextString())); + } catch (ParseException e) { + logger.error("Couldn't parse JWK Set", e); + } + } else if (name.equals(REQUEST_OBJECT_SIGNING_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setRequestObjectSigningAlg(alg); + } else if (name.equals(USER_INFO_ENCRYPTED_RESPONSE_ALG)) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setUserInfoEncryptedResponseAlg(alg); + } else if (name.equals(USER_INFO_ENCRYPTED_RESPONSE_ENC)) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setUserInfoEncryptedResponseEnc(alg); + } else if (name.equals(USER_INFO_SIGNED_RESPONSE_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setUserInfoSignedResponseAlg(alg); + } else if (name.equals(ID_TOKEN_SIGNED_RESPONSE_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setIdTokenSignedResponseAlg(alg); + } else if (name.equals(ID_TOKEN_ENCRYPTED_RESPONSE_ALG)) { + JWEAlgorithm alg = JWEAlgorithm.parse(reader.nextString()); + client.setIdTokenEncryptedResponseAlg(alg); + } else if (name.equals(ID_TOKEN_ENCRYPTED_RESPONSE_ENC)) { + EncryptionMethod alg = EncryptionMethod.parse(reader.nextString()); + client.setIdTokenEncryptedResponseEnc(alg); + } else if (name.equals(TOKEN_ENDPOINT_AUTH_SIGNING_ALG)) { + JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); + client.setTokenEndpointAuthSigningAlg(alg); + } else if (name.equals(DEFAULT_MAX_AGE)) { + client.setDefaultMaxAge(reader.nextInt()); + } else if (name.equals(REQUIRE_AUTH_TIME)) { + client.setRequireAuthTime(reader.nextBoolean()); + } else if (name.equals(DEFAULT_ACR_VALUES)) { + Set defaultACRvalues = readSet(reader); + client.setDefaultACRvalues(defaultACRvalues); + } else if (name.equals("initiateLoginUri")) { + client.setInitiateLoginUri(reader.nextString()); + } else if (name.equals(POST_LOGOUT_REDIRECT_URI)) { + Set postLogoutUris = readSet(reader); + client.setPostLogoutRedirectUris(postLogoutUris); + } else if (name.equals(REQUEST_URIS)) { + Set requestUris = readSet(reader); + client.setRequestUris(requestUris); + } else if (name.equals(DESCRIPTION)) { + client.setClientDescription(reader.nextString()); + } else if (name.equals(ALLOW_INTROSPECTION)) { + client.setAllowIntrospection(reader.nextBoolean()); + } else if (name.equals(REUSE_REFRESH_TOKEN)) { + client.setReuseRefreshToken(reader.nextBoolean()); + } else if (name.equals(CLEAR_ACCESS_TOKENS_ON_REFRESH)) { + client.setClearAccessTokensOnRefresh(reader.nextBoolean()); + } else if (name.equals(DYNAMICALLY_REGISTERED)) { + client.setDynamicallyRegistered(reader.nextBoolean()); + } else if (name.equals(CODE_CHALLENGE_METHOD)) { + client.setCodeChallengeMethod(PKCEAlgorithm.parse(reader.nextString())); + } else if (name.equals(SOFTWARE_ID)) { + client.setSoftwareId(reader.nextString()); + } else if (name.equals(SOFTWARE_VERSION)) { + client.setSoftwareVersion(reader.nextString()); + } else if (name.equals(SOFTWARE_STATEMENT)) { + try { + client.setSoftwareStatement(JWTParser.parse(reader.nextString())); + } catch (ParseException e) { + logger.error("Couldn't parse software statement", e); + } + } else if (name.equals(CREATION_DATE)) { + Date date = utcToDate(reader.nextString()); + client.setCreatedAt(date); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + clientRepository.saveClient(client); + } + reader.endArray(); + logger.info("Done reading clients"); + } + + /** + * Read the list of system scopes from the reader and insert them into the + * scope repository. + * + * @param reader + * @throws IOException + */ + private void readSystemScopes(JsonReader reader) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + SystemScope scope = new SystemScope(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(VALUE)) { + scope.setValue(reader.nextString()); + } else if (name.equals(DESCRIPTION)) { + scope.setDescription(reader.nextString()); + } else if (name.equals(RESTRICTED)) { + scope.setRestricted(reader.nextBoolean()); + } else if (name.equals(DEFAULT_SCOPE)) { + scope.setDefaultScope(reader.nextBoolean()); + } else if (name.equals(ICON)) { + scope.setIcon(reader.nextString()); + } else { + logger.debug("found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + sysScopeRepository.save(scope); + } + reader.endArray(); + logger.info("Done reading system scopes"); + } + + private void fixObjectReferences() { + logger.info("Fixing object references..."); + for (Long oldRefreshTokenId : maps.getRefreshTokenToClientRefs().keySet()) { + String clientRef = maps.getRefreshTokenToClientRefs().get(oldRefreshTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setClient(client); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldRefreshTokenId : maps.getRefreshTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getRefreshTokenToAuthHolderRefs().get(oldRefreshTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + refreshToken.setAuthenticationHolder(authHolder); + tokenRepository.saveRefreshToken(refreshToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToClientRefs().keySet()) { + String clientRef = maps.getAccessTokenToClientRefs().get(oldAccessTokenId); + ClientDetailsEntity client = clientRepository.getClientByClientId(clientRef); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setClient(client); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToAuthHolderRefs().keySet()) { + Long oldAuthHolderId = maps.getAccessTokenToAuthHolderRefs().get(oldAccessTokenId); + Long newAuthHolderId = maps.getAuthHolderOldToNewIdMap().get(oldAuthHolderId); + AuthenticationHolderEntity authHolder = authHolderRepository.getById(newAuthHolderId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setAuthenticationHolder(authHolder); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldAccessTokenId : maps.getAccessTokenToRefreshTokenRefs().keySet()) { + Long oldRefreshTokenId = maps.getAccessTokenToRefreshTokenRefs().get(oldAccessTokenId); + Long newRefreshTokenId = maps.getRefreshTokenOldToNewIdMap().get(oldRefreshTokenId); + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenById(newRefreshTokenId); + Long newAccessTokenId = maps.getAccessTokenOldToNewIdMap().get(oldAccessTokenId); + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenById(newAccessTokenId); + accessToken.setRefreshToken(refreshToken); + tokenRepository.saveAccessToken(accessToken); + } + for (Long oldGrantId : maps.getGrantToAccessTokensRefs().keySet()) { + Set oldAccessTokenIds = maps.getGrantToAccessTokensRefs().get(oldGrantId); + + Long newGrantId = maps.getGrantOldToNewIdMap().get(oldGrantId); + ApprovedSite site = approvedSiteRepository.getById(newGrantId); + + for(Long oldTokenId : oldAccessTokenIds) { + Long newTokenId = maps.getAccessTokenOldToNewIdMap().get(oldTokenId); + OAuth2AccessTokenEntity token = tokenRepository.getAccessTokenById(newTokenId); + token.setApprovedSite(site); + tokenRepository.saveAccessToken(token); + } + + approvedSiteRepository.save(site); + } + /* + refreshTokenToClientRefs.clear(); + refreshTokenToAuthHolderRefs.clear(); + accessTokenToClientRefs.clear(); + accessTokenToAuthHolderRefs.clear(); + accessTokenToRefreshTokenRefs.clear(); + refreshTokenOldToNewIdMap.clear(); + accessTokenOldToNewIdMap.clear(); + grantOldToNewIdMap.clear(); + */ + logger.info("Done fixing object references."); + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MatchLoginHintsAgainstUsers.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MatchLoginHintsAgainstUsers.java new file mode 100644 index 0000000000..74d98ea31b --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MatchLoginHintsAgainstUsers.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import org.mitre.openid.connect.model.UserInfo; +import org.mitre.openid.connect.service.LoginHintExtracter; +import org.mitre.openid.connect.service.UserInfoService; +import org.springframework.beans.factory.annotation.Autowired; + +import com.google.common.base.Strings; + +/** + * Checks the login hint against the User Info collection, only populates it if a user is found. + * @author jricher + * + */ +public class MatchLoginHintsAgainstUsers implements LoginHintExtracter { + + @Autowired + private UserInfoService userInfoService; + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.LoginHintTester#useHint(java.lang.String) + */ + @Override + public String extractHint(String loginHint) { + if (Strings.isNullOrEmpty(loginHint)) { + return null; + } else { + UserInfo user = userInfoService.getByEmailAddress(loginHint); + if (user == null) { + user = userInfoService.getByUsername(loginHint); + if (user == null) { + return null; + } else { + return user.getPreferredUsername(); + } + } else { + return user.getPreferredUsername(); + } + } + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/PassAllLoginHints.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/PassAllLoginHints.java new file mode 100644 index 0000000000..b1894466b6 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/PassAllLoginHints.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import org.mitre.openid.connect.service.LoginHintExtracter; + +/** + * Sends all login hints through to the login page regardless of setup. + * + * @author jricher + * + */ +public class PassAllLoginHints implements LoginHintExtracter { + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.LoginHintTester#useHint(java.lang.String) + */ + @Override + public String extractHint(String loginHint) { + return loginHint; + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/RemoveLoginHintsWithHTTP.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/RemoveLoginHintsWithHTTP.java new file mode 100644 index 0000000000..eab1585c20 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/RemoveLoginHintsWithHTTP.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import org.mitre.openid.connect.service.LoginHintExtracter; + +import com.google.common.base.Strings; + +/** + * Passes login hints that don't start with "http" + * + * @author jricher + * + */ +public class RemoveLoginHintsWithHTTP implements LoginHintExtracter { + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.LoginHintTester#useHint(java.lang.String) + */ + @Override + public String extractHint(String loginHint) { + if (Strings.isNullOrEmpty(loginHint)) { + return null; + } else { + if (loginHint.startsWith("http")) { + return null; + } else { + return loginHint; + } + } + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/UUIDPairwiseIdentiferService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/UUIDPairwiseIdentiferService.java index 5afd7d0df3..14b46fcc36 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/UUIDPairwiseIdentiferService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/UUIDPairwiseIdentiferService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.service.impl; @@ -44,7 +45,10 @@ @Service("uuidPairwiseIdentiferService") public class UUIDPairwiseIdentiferService implements PairwiseIdentiferService { - private static Logger logger = LoggerFactory.getLogger(UUIDPairwiseIdentiferService.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(UUIDPairwiseIdentiferService.class); @Autowired private PairwiseIdentifierRepository pairwiseIdentifierRepository; diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/token/ConnectTokenEnhancer.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/token/ConnectTokenEnhancer.java index 5ee4cde34d..d5b6a9cdc0 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/token/ConnectTokenEnhancer.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/token/ConnectTokenEnhancer.java @@ -1,33 +1,32 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.token; import java.util.Date; import java.util.UUID; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; -import org.mitre.jwt.signer.service.impl.JWKSetCacheService; -import org.mitre.jwt.signer.service.impl.SymmetricCacheService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.SystemScopeService; import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.mitre.openid.connect.model.UserInfo; -import org.mitre.openid.connect.service.ApprovedSiteService; import org.mitre.openid.connect.service.OIDCTokenService; import org.mitre.openid.connect.service.UserInfoService; import org.slf4j.Logger; @@ -39,42 +38,38 @@ import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.stereotype.Service; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTClaimsSet.Builder; import com.nimbusds.jwt.SignedJWT; @Service public class ConnectTokenEnhancer implements TokenEnhancer { - Logger logger = LoggerFactory.getLogger(ConnectTokenEnhancer.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ConnectTokenEnhancer.class); @Autowired private ConfigurationPropertiesBean configBean; @Autowired - private JwtSigningAndValidationService jwtService; + private JWTSigningAndValidationService jwtService; @Autowired private ClientDetailsEntityService clientService; - @Autowired - private ApprovedSiteService approvedSiteService; - @Autowired private UserInfoService userInfoService; @Autowired private OIDCTokenService connectTokenService; - @Autowired - private JWKSetCacheService encryptors; - - @Autowired - private SymmetricCacheService symmetricCacheService; - - @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { @@ -84,21 +79,28 @@ public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentica String clientId = originalAuthRequest.getClientId(); ClientDetailsEntity client = clientService.loadClientByClientId(clientId); - JWTClaimsSet claims = new JWTClaimsSet(); - - claims.setAudience(Lists.newArrayList(clientId)); - - claims.setIssuer(configBean.getIssuer()); - - claims.setIssueTime(new Date()); + Builder builder = new JWTClaimsSet.Builder() + .claim("azp", clientId) + .issuer(configBean.getIssuer()) + .issueTime(new Date()) + .expirationTime(token.getExpiration()) + .subject(authentication.getName()) + .jwtID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it + + String audience = (String) authentication.getOAuth2Request().getExtensions().get("aud"); + if (!Strings.isNullOrEmpty(audience)) { + builder.audience(Lists.newArrayList(audience)); + } - claims.setExpirationTime(token.getExpiration()); + addCustomAccessTokenClaims(builder, token, authentication); - claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it + JWTClaimsSet claims = builder.build(); JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm(); - - SignedJWT signed = new SignedJWT(new JWSHeader(signingAlg), claims); + JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null, + jwtService.getDefaultSignerKeyId(), + null, null); + SignedJWT signed = new SignedJWT(header, claims); jwtService.signJwt(signed); @@ -109,11 +111,11 @@ public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentica * may or may not include the scope parameter. As long as the AuthorizationRequest * has the proper scope, we can consider this a valid OpenID Connect request. Otherwise, * we consider it to be a vanilla OAuth2 request. - * + * * Also, there must be a user authentication involved in the request for it to be considered * OIDC and not OAuth, so we check for that as well. */ - if (originalAuthRequest.getScope().contains("openid") + if (originalAuthRequest.getScope().contains(SystemScopeService.OPENID_SCOPE) && !authentication.isClientOnly()) { String username = authentication.getName(); @@ -121,12 +123,12 @@ public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentica if (userInfo != null) { - OAuth2AccessTokenEntity idTokenEntity = connectTokenService.createIdToken(client, + JWT idToken = connectTokenService.createIdToken(client, originalAuthRequest, claims.getIssueTime(), userInfo.getSub(), token); // attach the id token to the parent access token - token.setIdToken(idTokenEntity); + token.setIdToken(idToken); } else { // can't create an id token if we can't find the user logger.warn("Request for ID token when no user is present."); @@ -144,11 +146,11 @@ public void setConfigBean(ConfigurationPropertiesBean configBean) { this.configBean = configBean; } - public JwtSigningAndValidationService getJwtService() { + public JWTSigningAndValidationService getJwtService() { return jwtService; } - public void setJwtService(JwtSigningAndValidationService jwtService) { + public void setJwtService(JWTSigningAndValidationService jwtService) { this.jwtService = jwtService; } @@ -160,4 +162,15 @@ public void setClientService(ClientDetailsEntityService clientService) { this.clientService = clientService; } + + /** + * Hook for subclasses that allows adding custom claims to the JWT that will be used as access token. + * @param builder the builder holding the current claims + * @param token the un-enhanced token + * @param authentication current authentication + */ + protected void addCustomAccessTokenClaims(JWTClaimsSet.Builder builder, OAuth2AccessTokenEntity token, + OAuth2Authentication authentication) { + } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/token/TofuUserApprovalHandler.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/token/TofuUserApprovalHandler.java index df95a6aff2..821b1fafb6 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/token/TofuUserApprovalHandler.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/token/TofuUserApprovalHandler.java @@ -1,21 +1,27 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.token; +import static org.mitre.openid.connect.request.ConnectRequestParameters.APPROVED_SITE; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT_CONSENT; +import static org.mitre.openid.connect.request.ConnectRequestParameters.PROMPT_SEPARATOR; + import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -26,7 +32,6 @@ import javax.servlet.http.HttpSession; -import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.service.SystemScopeService; import org.mitre.openid.connect.model.ApprovedSite; import org.mitre.openid.connect.model.WhitelistedSite; @@ -50,13 +55,13 @@ /** * Custom User Approval Handler implementation which uses a concept of a whitelist, * blacklist, and greylist. - * + * * Blacklisted sites will be caught and handled before this * point. - * + * * Whitelisted sites will be automatically approved, and an ApprovedSite entry will * be created for the site the first time a given user access it. - * + * * All other sites fall into the greylist - the user will be presented with the user * approval page upon their first visit * @author aanganes @@ -80,12 +85,12 @@ public class TofuUserApprovalHandler implements UserApprovalHandler { /** * Check if the user has already stored a positive approval decision for this site; or if the * site is whitelisted, approve it automatically. - * + * * Otherwise, return false so that the user will see the approval page and can make their own decision. - * + * * @param authorizationRequest the incoming authorization request * @param userAuthentication the Principal representing the currently-logged-in user - * + * * @return true if the site is approved, false otherwise */ @Override @@ -97,21 +102,8 @@ public boolean isApproved(AuthorizationRequest authorizationRequest, Authenticat return true; } else { // if not, check to see if the user has approved it - if (Boolean.parseBoolean(authorizationRequest.getApprovalParameters().get("user_oauth_approval"))) { // TODO: make parameter name configurable? - - // check the value of the CSRF parameter - - if (authorizationRequest.getExtensions().get("csrf") != null) { - if (authorizationRequest.getExtensions().get("csrf").equals(authorizationRequest.getApprovalParameters().get("csrf"))) { - - // make sure the user is actually authenticated - return userAuthentication.isAuthenticated(); - } - } - } - - // if the above doesn't pass, it's not yet approved - return false; + // TODO: make parameter name configurable? + return Boolean.parseBoolean(authorizationRequest.getApprovalParameters().get("user_oauth_approval")); } } @@ -119,12 +111,12 @@ public boolean isApproved(AuthorizationRequest authorizationRequest, Authenticat /** * Check if the user has already stored a positive approval decision for this site; or if the * site is whitelisted, approve it automatically. - * + * * Otherwise the user will be directed to the approval page and can make their own decision. - * + * * @param authorizationRequest the incoming authorization request * @param userAuthentication the Principal representing the currently-logged-in user - * + * * @return the updated AuthorizationRequest */ @Override @@ -139,9 +131,9 @@ public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizati boolean alreadyApproved = false; // find out if we're supposed to force a prompt on the user or not - String prompt = (String) authorizationRequest.getExtensions().get("prompt"); - List prompts = Splitter.on(" ").splitToList(Strings.nullToEmpty(prompt)); - if (!prompts.contains("consent")) { + String prompt = (String) authorizationRequest.getExtensions().get(PROMPT); + List prompts = Splitter.on(PROMPT_SEPARATOR).splitToList(Strings.nullToEmpty(prompt)); + if (!prompts.contains(PROMPT_CONSENT)) { // if the prompt parameter is set to "consent" then we can't use approved sites or whitelisted sites // otherwise, we need to check them below @@ -157,7 +149,8 @@ public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizati ap.setAccessDate(new Date()); approvedSiteService.save(ap); - authorizationRequest.getExtensions().put("approved_site", ap.getId()); + String apId = ap.getId().toString(); + authorizationRequest.getExtensions().put(APPROVED_SITE, apId); authorizationRequest.setApproved(true); alreadyApproved = true; @@ -169,10 +162,6 @@ public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizati if (!alreadyApproved) { WhitelistedSite ws = whitelistedSiteService.getByClientId(clientId); if (ws != null && systemScopes.scopesMatch(ws.getAllowedScopes(), authorizationRequest.getScope())) { - - //Create an approved site - ApprovedSite newSite = approvedSiteService.createApprovedSite(clientId, userId, null, ws.getAllowedScopes(), ws); - authorizationRequest.getExtensions().put("approved_site", newSite.getId()); authorizationRequest.setApproved(true); setAuthTime(authorizationRequest); @@ -193,9 +182,7 @@ public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizati ClientDetails client = clientDetailsService.loadClientByClientId(clientId); // This must be re-parsed here because SECOAUTH forces us to call things in a strange order - if (Boolean.parseBoolean(authorizationRequest.getApprovalParameters().get("user_oauth_approval")) - && authorizationRequest.getExtensions().get("csrf") != null - && authorizationRequest.getExtensions().get("csrf").equals(authorizationRequest.getApprovalParameters().get("csrf"))) { + if (Boolean.parseBoolean(authorizationRequest.getApprovalParameters().get("user_oauth_approval"))) { authorizationRequest.setApproved(true); @@ -217,15 +204,7 @@ public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizati //Make sure this scope is allowed for the given client if (systemScopes.scopesMatch(client.getScope(), approveSet)) { - // If it's structured, assign the user-specified parameter - SystemScope systemScope = systemScopes.getByValue(scope); - if (systemScope != null && systemScope.isStructured()){ - String paramValue = approvalParams.get("scopeparam_" + scope); - allowedScopes.add(scope + ":"+paramValue); - // .. and if it's unstructured, we're all set - } else { - allowedScopes.add(scope); - } + allowedScopes.add(scope); } } @@ -246,8 +225,9 @@ public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizati timeout = cal.getTime(); } - ApprovedSite newSite = approvedSiteService.createApprovedSite(clientId, userId, timeout, allowedScopes, null); - authorizationRequest.getExtensions().put("approved_site", newSite.getId()); + ApprovedSite newSite = approvedSiteService.createApprovedSite(clientId, userId, timeout, allowedScopes); + String newSiteId = newSite.getId().toString(); + authorizationRequest.getExtensions().put(APPROVED_SITE, newSiteId); } setAuthTime(authorizationRequest); @@ -261,7 +241,7 @@ public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizati /** * Get the auth time out of the current session and add it to the * auth request in the extensions map. - * + * * @param authorizationRequest */ private void setAuthTime(AuthorizationRequest authorizationRequest) { @@ -272,16 +252,17 @@ private void setAuthTime(AuthorizationRequest authorizationRequest) { if (session != null) { Date authTime = (Date) session.getAttribute(AuthenticationTimeStamper.AUTH_TIMESTAMP); if (authTime != null) { - authorizationRequest.getExtensions().put(AuthenticationTimeStamper.AUTH_TIMESTAMP, authTime); + String authTimeString = Long.toString(authTime.getTime()); + authorizationRequest.getExtensions().put(AuthenticationTimeStamper.AUTH_TIMESTAMP, authTimeString); } } } } - + @Override public Map getUserApprovalRequest(AuthorizationRequest authorizationRequest, Authentication userAuthentication) { - Map model = new HashMap(); + Map model = new HashMap<>(); // In case of a redirect we might want the request parameters to be included model.putAll(authorizationRequest.getRequestParameters()); return model; diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/util/IdTokenHashUtils.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/util/IdTokenHashUtils.java index 70761d73f4..789f3ce184 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/util/IdTokenHashUtils.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/util/IdTokenHashUtils.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.util; @@ -31,17 +32,20 @@ /** * Utility class for generating hashes for access tokens and authorization codes * to be included in an ID Token. - * + * * @author Amanda Anganes - * + * */ public class IdTokenHashUtils { - private static Logger logger = LoggerFactory.getLogger(IdTokenHashUtils.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(IdTokenHashUtils.class); /** * Compute the SHA hash of an authorization code - * + * * @param signingAlg * @param code * @return @@ -52,7 +56,7 @@ public static Base64URL getCodeHash(JWSAlgorithm signingAlg, String code) { /** * Compute the SHA hash of a token - * + * * @param signingAlg * @param token * @return @@ -71,15 +75,15 @@ public static Base64URL getHash(JWSAlgorithm signingAlg, byte[] bytes) { //as the JWSAlgorithm to hash the token. String hashAlg = null; - if (signingAlg.equals(JWSAlgorithm.HS256) || signingAlg.equals(JWSAlgorithm.ES256) || signingAlg.equals(JWSAlgorithm.RS256)) { + if (signingAlg.equals(JWSAlgorithm.HS256) || signingAlg.equals(JWSAlgorithm.ES256) || signingAlg.equals(JWSAlgorithm.RS256) || signingAlg.equals(JWSAlgorithm.PS256)) { hashAlg = "SHA-256"; } - else if (signingAlg.equals(JWSAlgorithm.ES384) || signingAlg.equals(JWSAlgorithm.HS384) || signingAlg.equals(JWSAlgorithm.RS384)) { + else if (signingAlg.equals(JWSAlgorithm.ES384) || signingAlg.equals(JWSAlgorithm.HS384) || signingAlg.equals(JWSAlgorithm.RS384) || signingAlg.equals(JWSAlgorithm.PS384)) { hashAlg = "SHA-384"; } - else if (signingAlg.equals(JWSAlgorithm.ES512) || signingAlg.equals(JWSAlgorithm.HS512) || signingAlg.equals(JWSAlgorithm.RS512)) { + else if (signingAlg.equals(JWSAlgorithm.ES512) || signingAlg.equals(JWSAlgorithm.HS512) || signingAlg.equals(JWSAlgorithm.RS512) || signingAlg.equals(JWSAlgorithm.PS512)) { hashAlg = "SHA-512"; } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java index 70aa24e9a4..719bfc8d0e 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -27,70 +28,111 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.mitre.jose.JWEAlgorithmEmbed; -import org.mitre.jose.JWEEncryptionMethodEmbed; -import org.mitre.jose.JWSAlgorithmEmbed; +import org.mitre.oauth2.model.PKCEAlgorithm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.web.servlet.view.AbstractView; import com.google.gson.ExclusionStrategy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWT; /** - * + * * Abstract superclass for client entity view, used with the ClientApi. - * + * * @see ClientEntityViewForUsers * @see ClientEntityViewForAdmins - * + * * @author jricher * */ public abstract class AbstractClientEntityView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(ClientEntityViewForAdmins.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(AbstractClientEntityView.class); + + private JsonParser parser = new JsonParser(); private Gson gson = new GsonBuilder() - .setExclusionStrategies(getExclusionStrategy()) - .registerTypeAdapter(JWSAlgorithmEmbed.class, new JsonSerializer() { - @Override - public JsonElement serialize(JWSAlgorithmEmbed src, Type typeOfSrc, JsonSerializationContext context) { - if (src != null) { - return new JsonPrimitive(src.getAlgorithmName()); - } else { - return null; - } - } - }) - .registerTypeAdapter(JWEAlgorithmEmbed.class, new JsonSerializer() { - @Override - public JsonElement serialize(JWEAlgorithmEmbed src, Type typeOfSrc, JsonSerializationContext context) { - if (src != null) { - return new JsonPrimitive(src.getAlgorithmName()); - } else { - return null; - } - } - }) - .registerTypeAdapter(JWEEncryptionMethodEmbed.class, new JsonSerializer() { - @Override - public JsonElement serialize(JWEEncryptionMethodEmbed src, Type typeOfSrc, JsonSerializationContext context) { - if (src != null) { - return new JsonPrimitive(src.getAlgorithmName()); - } else { - return null; - } - } - }) - .serializeNulls() - .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .create(); + .setExclusionStrategies(getExclusionStrategy()) + .registerTypeAdapter(JWSAlgorithm.class, new JsonSerializer() { + @Override + public JsonElement serialize(JWSAlgorithm src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return new JsonPrimitive(src.getName()); + } else { + return null; + } + } + }) + .registerTypeAdapter(JWEAlgorithm.class, new JsonSerializer() { + @Override + public JsonElement serialize(JWEAlgorithm src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return new JsonPrimitive(src.getName()); + } else { + return null; + } + } + }) + .registerTypeAdapter(EncryptionMethod.class, new JsonSerializer() { + @Override + public JsonElement serialize(EncryptionMethod src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return new JsonPrimitive(src.getName()); + } else { + return null; + } + } + }) + .registerTypeAdapter(JWKSet.class, new JsonSerializer() { + @Override + public JsonElement serialize(JWKSet src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return parser.parse(src.toString()); + } else { + return null; + } + } + }) + .registerTypeAdapter(JWT.class, new JsonSerializer() { + @Override + public JsonElement serialize(JWT src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return new JsonPrimitive(src.serialize()); + } else { + return null; + } + } + + }) + .registerTypeAdapter(PKCEAlgorithm.class, new JsonSerializer() { + @Override + public JsonPrimitive serialize(PKCEAlgorithm src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return new JsonPrimitive(src.getName()); + } else { + return null; + } + } + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); /** @@ -102,10 +144,10 @@ public JsonElement serialize(JWEEncryptionMethodEmbed src, Type typeOfSrc, JsonS @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); if (code == null) { code = HttpStatus.OK; // default to 200 } @@ -115,7 +157,7 @@ protected void renderMergedOutputModel(Map model, HttpServletReq try { Writer out = response.getWriter(); - Object obj = model.get("entity"); + Object obj = model.get(JsonEntityView.ENTITY); gson.toJson(obj, out); } catch (IOException e) { diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForAdmins.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForAdmins.java index ec45bef944..7fe86f7f77 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForAdmins.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForAdmins.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -29,16 +30,17 @@ import com.google.gson.FieldAttributes; /** - * + * * View bean for full view of client entity, for admins. - * + * * @see ClientEntityViewForUsers * @author jricher * */ -@Component("clientEntityViewAdmins") +@Component(ClientEntityViewForAdmins.VIEWNAME) public class ClientEntityViewForAdmins extends AbstractClientEntityView { + public static final String VIEWNAME = "clientEntityViewAdmins"; private Set blacklistedFields = ImmutableSet.of("additionalInformation"); /** diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForUsers.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForUsers.java index 65f02b21fc..11f1e51dc4 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForUsers.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientEntityViewForUsers.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -29,19 +30,21 @@ import com.google.gson.FieldAttributes; /** - * + * * View bean for field-limited view of client entity, for regular users. - * + * * @see AbstractClientEntityView * @see ClientEntityViewForAdmins * @author jricher * */ -@Component("clientEntityViewUsers") +@Component(ClientEntityViewForUsers.VIEWNAME) public class ClientEntityViewForUsers extends AbstractClientEntityView { private Set whitelistedFields = ImmutableSet.of("clientName", "clientId", "id", "clientDescription", "scope", "logoUri"); + public static final String VIEWNAME = "clientEntityViewUsers"; + /* (non-Javadoc) * @see org.mitre.openid.connect.view.AbstractClientEntityView#getExclusionStrategy() */ diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientInformationResponseView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientInformationResponseView.java index 5d05a5f9eb..a4b245179e 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientInformationResponseView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/ClientInformationResponseView.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -31,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.servlet.view.AbstractView; @@ -39,17 +41,22 @@ import com.google.gson.JsonObject; /** - * + * * Provides representation of a client's registration metadata, to be shown from the dynamic registration endpoint * on the client_register and rotate_secret operations. - * + * * @author jricher * */ -@Component("clientInformationResponseView") +@Component(ClientInformationResponseView.VIEWNAME) public class ClientInformationResponseView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(ClientInformationResponseView.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ClientInformationResponseView.class); + + public static final String VIEWNAME = "clientInformationResponseView"; // note that this won't serialize nulls by default private Gson gson = new Gson(); @@ -60,16 +67,17 @@ public class ClientInformationResponseView extends AbstractView { @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); RegisteredClient c = (RegisteredClient) model.get("client"); //OAuth2AccessTokenEntity token = (OAuth2AccessTokenEntity) model.get("token"); //String uri = (String)model.get("uri"); //request.getRequestURL() + "/" + c.getClientId(); - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); if (code == null) { code = HttpStatus.OK; } + response.setStatus(code.value()); JsonObject o = ClientDetailsEntityJsonProcessor.serialize(c); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/HttpCodeView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/HttpCodeView.java index cc14d738e8..bea53cd173 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/HttpCodeView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/HttpCodeView.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -33,12 +34,16 @@ * @author jricher * */ -@Component("httpCodeView") +@Component(HttpCodeView.VIEWNAME) public class HttpCodeView extends AbstractView { + public static final String VIEWNAME = "httpCodeView"; + + public static final String CODE = "code"; + @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(CODE); if (code == null) { code = HttpStatus.OK; // default to 200 } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonApprovedSiteView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonApprovedSiteView.java index c3a99573b1..4b84f1a8cf 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonApprovedSiteView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonApprovedSiteView.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -32,6 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.web.servlet.view.AbstractView; @@ -49,54 +51,59 @@ * @author jricher * */ -@Component("jsonApprovedSiteView") +@Component(JsonApprovedSiteView.VIEWNAME) public class JsonApprovedSiteView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(JsonApprovedSiteView.class); - - private Gson gson = new GsonBuilder() - .setExclusionStrategies(new ExclusionStrategy() { - - @Override - public boolean shouldSkipField(FieldAttributes f) { - - return false; - } + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JsonApprovedSiteView.class); - @Override - public boolean shouldSkipClass(Class clazz) { - // skip the JPA binding wrapper - if (clazz.equals(BeanPropertyBindingResult.class)) { - return true; - } - return false; - } + public static final String VIEWNAME = "jsonApprovedSiteView"; - }) - .registerTypeAdapter(OAuth2AccessTokenEntity.class, new JsonSerializer() { - @Override - public JsonElement serialize(OAuth2AccessTokenEntity src, - Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.getId()); - } - }) - .registerTypeAdapter(WhitelistedSite.class, new JsonSerializer() { - @Override - public JsonElement serialize(WhitelistedSite src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.getId()); - } - }) - .serializeNulls() - .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .create(); + private Gson gson = new GsonBuilder() + .setExclusionStrategies(new ExclusionStrategy() { + + @Override + public boolean shouldSkipField(FieldAttributes f) { + + return false; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } + return false; + } + + }) + .registerTypeAdapter(OAuth2AccessTokenEntity.class, new JsonSerializer() { + @Override + public JsonElement serialize(OAuth2AccessTokenEntity src, + Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getId()); + } + }) + .registerTypeAdapter(WhitelistedSite.class, new JsonSerializer() { + @Override + public JsonElement serialize(WhitelistedSite src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getId()); + } + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); if (code == null) { code = HttpStatus.OK; // default to 200 } @@ -106,7 +113,7 @@ protected void renderMergedOutputModel(Map model, HttpServletReq try { Writer out = response.getWriter(); - Object obj = model.get("entity"); + Object obj = model.get(JsonEntityView.ENTITY); gson.toJson(obj, out); } catch (IOException e) { diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonEntityView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonEntityView.java index c72e88b637..a9e9401c67 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonEntityView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonEntityView.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; @@ -29,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.web.servlet.view.AbstractView; @@ -42,41 +44,49 @@ * @author jricher * */ -@Component("jsonEntityView") +@Component(JsonEntityView.VIEWNAME) public class JsonEntityView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(JsonEntityView.class); + public static final String ENTITY = "entity"; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JsonEntityView.class); + + public static final String VIEWNAME = "jsonEntityView"; private Gson gson = new GsonBuilder() - .setExclusionStrategies(new ExclusionStrategy() { + .setExclusionStrategies(new ExclusionStrategy() { - @Override - public boolean shouldSkipField(FieldAttributes f) { + @Override + public boolean shouldSkipField(FieldAttributes f) { - return false; - } + return false; + } - @Override - public boolean shouldSkipClass(Class clazz) { - // skip the JPA binding wrapper - if (clazz.equals(BeanPropertyBindingResult.class)) { - return true; - } - return false; - } + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } + return false; + } - }) - .serializeNulls() - .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .create(); + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); if (code == null) { code = HttpStatus.OK; // default to 200 } @@ -86,7 +96,7 @@ protected void renderMergedOutputModel(Map model, HttpServletReq try { Writer out = response.getWriter(); - Object obj = model.get("entity"); + Object obj = model.get(ENTITY); gson.toJson(obj, out); } catch (IOException e) { diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonErrorView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonErrorView.java index 49104bf682..db21fffb26 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonErrorView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/JsonErrorView.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; import java.io.IOException; @@ -26,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.web.servlet.view.AbstractView; @@ -41,43 +43,58 @@ * @author aanganes, jricher * */ -@Component("jsonErrorView") +@Component(JsonErrorView.VIEWNAME) public class JsonErrorView extends AbstractView { - private static Logger logger = LoggerFactory.getLogger(JsonEntityView.class); + /** + * + */ + public static final String ERROR_MESSAGE = "errorMessage"; + + /** + * + */ + public static final String ERROR = "error"; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(JsonErrorView.class); + + public static final String VIEWNAME = "jsonErrorView"; private Gson gson = new GsonBuilder() - .setExclusionStrategies(new ExclusionStrategy() { + .setExclusionStrategies(new ExclusionStrategy() { - @Override - public boolean shouldSkipField(FieldAttributes f) { + @Override + public boolean shouldSkipField(FieldAttributes f) { - return false; - } + return false; + } - @Override - public boolean shouldSkipClass(Class clazz) { - // skip the JPA binding wrapper - if (clazz.equals(BeanPropertyBindingResult.class)) { - return true; - } - return false; - } + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } + return false; + } - }) - .serializeNulls() - .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .create(); + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); - HttpStatus code = (HttpStatus) model.get("code"); + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); if (code == null) { - code = HttpStatus.OK; // default to 200 + code = HttpStatus.INTERNAL_SERVER_ERROR; // default to 500 } response.setStatus(code.value()); @@ -86,11 +103,11 @@ protected void renderMergedOutputModel(Map model, HttpServletReq Writer out = response.getWriter(); - String errorTitle = (String) model.get("error"); + String errorTitle = (String) model.get(ERROR); if (Strings.isNullOrEmpty(errorTitle)) { - errorTitle = "Error"; + errorTitle = "mitreid_error"; } - String errorMessage = (String) model.get("errorMessage"); + String errorMessage = (String) model.get(ERROR_MESSAGE); JsonObject obj = new JsonObject(); obj.addProperty("error", errorTitle); obj.addProperty("error_description", errorMessage); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJwtView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJWTView.java similarity index 60% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJwtView.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJWTView.java index ddb8e84d50..e452b352b5 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJwtView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJWTView.java @@ -1,6 +1,5 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +14,7 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.view; @@ -30,15 +29,16 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; -import org.mitre.jwt.signer.service.impl.JWKSetCacheService; -import org.mitre.jwt.signer.service.impl.SymmetricCacheService; +import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.ClientKeyCacheService; +import org.mitre.jwt.signer.service.impl.SymmetricKeyJWTValidatorCacheService; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import com.google.common.base.Strings; @@ -56,58 +56,66 @@ * @author jricher * */ -@Component("userInfoJwtView") -public class UserInfoJwtView extends UserInfoView { +@Component(UserInfoJWTView.VIEWNAME) +public class UserInfoJWTView extends UserInfoView { + + public static final String CLIENT = "client"; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(UserInfoJWTView.class); + + public static final String VIEWNAME = "userInfoJwtView"; + + public static final String JOSE_MEDIA_TYPE_VALUE = "application/jwt"; + public static final MediaType JOSE_MEDIA_TYPE = new MediaType("application", "jwt"); - private static Logger logger = LoggerFactory.getLogger(UserInfoJwtView.class); @Autowired - private JwtSigningAndValidationService jwtService; + private JWTSigningAndValidationService jwtService; @Autowired private ConfigurationPropertiesBean config; @Autowired - private JWKSetCacheService encrypters; + private ClientKeyCacheService encrypters; @Autowired - private SymmetricCacheService symmetricCacheService; + private SymmetricKeyJWTValidatorCacheService symmetricCacheService; @Override protected void writeOut(JsonObject json, Map model, HttpServletRequest request, HttpServletResponse response) { try { - ClientDetailsEntity client = (ClientDetailsEntity)model.get("client"); + ClientDetailsEntity client = (ClientDetailsEntity)model.get(CLIENT); // use the parser to import the user claims into the object StringWriter writer = new StringWriter(); gson.toJson(json, writer); - response.setContentType("application/jwt"); - - JWTClaimsSet claims = JWTClaimsSet.parse(writer.toString()); + response.setContentType(JOSE_MEDIA_TYPE_VALUE); - claims.setAudience(Lists.newArrayList(client.getClientId())); + JWTClaimsSet claims = new JWTClaimsSet.Builder(JWTClaimsSet.parse(writer.toString())) + .audience(Lists.newArrayList(client.getClientId())) + .issuer(config.getIssuer()) + .issueTime(new Date()) + .jwtID(UUID.randomUUID().toString()) // set a random NONCE in the middle of it + .build(); - claims.setIssuer(config.getIssuer()); - claims.setIssueTime(new Date()); - - claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it - - - if (client.getIdTokenEncryptedResponseAlg() != null && !client.getIdTokenEncryptedResponseAlg().equals(Algorithm.NONE) - && client.getIdTokenEncryptedResponseEnc() != null && !client.getIdTokenEncryptedResponseEnc().equals(Algorithm.NONE) - && !Strings.isNullOrEmpty(client.getJwksUri())) { + if (client.getUserInfoEncryptedResponseAlg() != null && !client.getUserInfoEncryptedResponseAlg().equals(Algorithm.NONE) + && client.getUserInfoEncryptedResponseEnc() != null && !client.getUserInfoEncryptedResponseEnc().equals(Algorithm.NONE) + && (!Strings.isNullOrEmpty(client.getJwksUri()) || client.getJwks() != null)) { // encrypt it to the client's key - JwtEncryptionAndDecryptionService encrypter = encrypters.getEncrypter(client.getJwksUri()); + JWTEncryptionAndDecryptionService encrypter = encrypters.getEncrypter(client); if (encrypter != null) { - EncryptedJWT encrypted = new EncryptedJWT(new JWEHeader(client.getIdTokenEncryptedResponseAlg(), client.getIdTokenEncryptedResponseEnc()), claims); + EncryptedJWT encrypted = new EncryptedJWT(new JWEHeader(client.getUserInfoEncryptedResponseAlg(), client.getUserInfoEncryptedResponseEnc()), claims); encrypter.encryptJwt(encrypted); @@ -124,15 +132,17 @@ protected void writeOut(JsonObject json, Map model, if (client.getUserInfoSignedResponseAlg() != null) { signingAlg = client.getUserInfoSignedResponseAlg(); // override with the client's preference if available } - - SignedJWT signed = new SignedJWT(new JWSHeader(signingAlg), claims); + JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null, + jwtService.getDefaultSignerKeyId(), + null, null); + SignedJWT signed = new SignedJWT(header, claims); if (signingAlg.equals(JWSAlgorithm.HS256) || signingAlg.equals(JWSAlgorithm.HS384) || signingAlg.equals(JWSAlgorithm.HS512)) { // sign it with the client's secret - JwtSigningAndValidationService signer = symmetricCacheService.getSymmetricValidtor(client); + JWTSigningAndValidationService signer = symmetricCacheService.getSymmetricValidtor(client); signer.signJwt(signed); } else { diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java index 9311538806..73ca617f98 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.view; import java.io.IOException; @@ -31,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.web.servlet.view.AbstractView; @@ -43,12 +45,22 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; -@Component("userInfoView") +@Component(UserInfoView.VIEWNAME) public class UserInfoView extends AbstractView { + public static final String REQUESTED_CLAIMS = "requestedClaims"; + public static final String AUTHORIZED_CLAIMS = "authorizedClaims"; + public static final String SCOPE = "scope"; + public static final String USER_INFO = "userInfo"; + + public static final String VIEWNAME = "userInfoView"; + private static JsonParser jsonParser = new JsonParser(); - private static Logger logger = LoggerFactory.getLogger(UserInfoView.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(UserInfoView.class); @Autowired private ScopeClaimTranslationService translator; @@ -74,7 +86,7 @@ public boolean shouldSkipClass(Class clazz) { /* * (non-Javadoc) - * + * * @see * org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel * (java.util.Map, javax.servlet.http.HttpServletRequest, @@ -83,20 +95,21 @@ public boolean shouldSkipClass(Class clazz) { @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { - UserInfo userInfo = (UserInfo) model.get("userInfo"); + UserInfo userInfo = (UserInfo) model.get(USER_INFO); - Set scope = (Set) model.get("scope"); + Set scope = (Set) model.get(SCOPE); - response.setContentType("application/json"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); JsonObject authorizedClaims = null; JsonObject requestedClaims = null; - if (model.get("authorizedClaims") != null) { - authorizedClaims = jsonParser.parse((String) model.get("authorizedClaims")).getAsJsonObject(); + if (model.get(AUTHORIZED_CLAIMS) != null) { + authorizedClaims = jsonParser.parse((String) model.get(AUTHORIZED_CLAIMS)).getAsJsonObject(); } - if (model.get("requestedClaims") != null) { - requestedClaims = jsonParser.parse((String) model.get("requestedClaims")).getAsJsonObject(); + if (model.get(REQUESTED_CLAIMS) != null) { + requestedClaims = jsonParser.parse((String) model.get(REQUESTED_CLAIMS)).getAsJsonObject(); } JsonObject json = toJsonFromRequestObj(userInfo, scope, authorizedClaims, requestedClaims); @@ -117,10 +130,10 @@ protected void writeOut(JsonObject json, Map model, HttpServletR /** * Build a JSON response according to the request object received. - * + * * Claims requested in requestObj.userinfo.claims are added to any * claims corresponding to requested scopes, if any. - * + * * @param ui the UserInfo to filter * @param scope the allowed scopes to filter by * @param authorizedClaims the claims authorized by the client or user @@ -133,21 +146,8 @@ private JsonObject toJsonFromRequestObj(UserInfo ui, Set scope, JsonObje JsonObject obj = ui.toJson(); Set allowedByScope = translator.getClaimsForScopeSet(scope); - Set authorizedByClaims = new HashSet(); - Set requestedByClaims = new HashSet(); - - if (authorizedClaims != null) { - JsonObject userinfoAuthorized = authorizedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); - for (Entry entry : userinfoAuthorized.getAsJsonObject().entrySet()) { - authorizedByClaims.add(entry.getKey()); - } - } - if (requestedClaims != null) { - JsonObject userinfoRequested = requestedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); - for (Entry entry : userinfoRequested.getAsJsonObject().entrySet()) { - requestedByClaims.add(entry.getKey()); - } - } + Set authorizedByClaims = extractUserInfoClaimsIntoSet(authorizedClaims); + Set requestedByClaims = extractUserInfoClaimsIntoSet(requestedClaims); // Filter claims by performing a manual intersection of claims that are allowed by the given scope, requested, and authorized. // We cannot use Sets.intersection() or similar because Entry<> objects will evaluate to being unequal if their values are @@ -168,4 +168,22 @@ private JsonObject toJsonFromRequestObj(UserInfo ui, Set scope, JsonObje return result; } + + /** + * Pull the claims that have been targeted into a set for processing. + * Returns an empty set if the input is null. + * @param claims the claims request to process + */ + private Set extractUserInfoClaimsIntoSet(JsonObject claims) { + Set target = new HashSet<>(); + if (claims != null) { + JsonObject userinfoAuthorized = claims.getAsJsonObject("userinfo"); + if (userinfoAuthorized != null) { + for (Entry entry : userinfoAuthorized.entrySet()) { + target.add(entry.getKey()); + } + } + } + return target; + } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ApprovedSiteAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ApprovedSiteAPI.java index a79eeaa4ae..1f05baab56 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ApprovedSiteAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ApprovedSiteAPI.java @@ -1,34 +1,39 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.security.Principal; import java.util.Collection; -import org.mitre.oauth2.service.OAuth2TokenEntityService; import org.mitre.openid.connect.model.ApprovedSite; import org.mitre.openid.connect.service.ApprovedSiteService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonApprovedSiteView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -41,36 +46,38 @@ * */ @Controller -@RequestMapping("/api/approved") +@RequestMapping("/" + ApprovedSiteAPI.URL) @PreAuthorize("hasRole('ROLE_USER')") public class ApprovedSiteAPI { - @Autowired - private ApprovedSiteService approvedSiteService; + public static final String URL = RootController.API_URL + "/approved"; @Autowired - OAuth2TokenEntityService tokenServices; + private ApprovedSiteService approvedSiteService; - private static Logger logger = LoggerFactory.getLogger(ApprovedSiteAPI.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ApprovedSiteAPI.class); /** * Get a list of all of this user's approved sites * @param m * @return */ - @RequestMapping(method = RequestMethod.GET, produces = "application/json") + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getAllApprovedSites(ModelMap m, Principal p) { Collection all = approvedSiteService.getByUserId(p.getName()); - m.put("entity", all); + m.put(JsonEntityView.ENTITY, all); - return "jsonApprovedSiteView"; + return JsonApprovedSiteView.VIEWNAME; } /** * Delete an approved site - * + * */ @RequestMapping(value="/{id}", method = RequestMethod.DELETE) public String deleteApprovedSite(@PathVariable("id") Long id, ModelMap m, Principal p) { @@ -78,44 +85,45 @@ public String deleteApprovedSite(@PathVariable("id") Long id, ModelMap m, Princi if (approvedSite == null) { logger.error("deleteApprovedSite failed; no approved site found for id: " + id); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "Could not delete approved site. The requested approved site with id: " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not delete approved site. The requested approved site with id: " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else if (!approvedSite.getUserId().equals(p.getName())) { logger.error("deleteApprovedSite failed; principal " + p.getName() + " does not own approved site" + id); - m.put("code", HttpStatus.FORBIDDEN); - m.put("errorMessage", "You do not have permission to delete this approved site. The approved site decision will not be deleted."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + m.put(JsonErrorView.ERROR_MESSAGE, "You do not have permission to delete this approved site. The approved site decision will not be deleted."); + return JsonErrorView.VIEWNAME; } else { - m.put("code", HttpStatus.OK); + m.put(HttpCodeView.CODE, HttpStatus.OK); approvedSiteService.remove(approvedSite); } - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } /** * Get a single approved site */ - @RequestMapping(value="/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value="/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getApprovedSite(@PathVariable("id") Long id, ModelMap m, Principal p) { ApprovedSite approvedSite = approvedSiteService.getById(id); if (approvedSite == null) { logger.error("getApprovedSite failed; no approved site found for id: " + id); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "The requested approved site with id: " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested approved site with id: " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else if (!approvedSite.getUserId().equals(p.getName())) { logger.error("getApprovedSite failed; principal " + p.getName() + " does not own approved site" + id); - m.put("code", HttpStatus.FORBIDDEN); - m.put("errorMessage", "You do not have permission to view this approved site."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + m.put(JsonErrorView.ERROR_MESSAGE, "You do not have permission to view this approved site."); + return JsonErrorView.VIEWNAME; } else { - m.put("entity", approvedSite); - return "jsonApprovedSiteView"; + m.put(JsonEntityView.ENTITY, approvedSite); + return JsonApprovedSiteView.VIEWNAME; } } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/AuthenticationTimeStamper.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/AuthenticationTimeStamper.java index 6c8f143e99..4b5bd4c2e0 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/AuthenticationTimeStamper.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/AuthenticationTimeStamper.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; @@ -27,7 +28,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import org.mitre.openid.connect.filter.PromptFilter; +import org.mitre.openid.connect.filter.AuthorizationRequestFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; @@ -37,14 +38,17 @@ /** * This class sets a timestamp on the current HttpSession * when someone successfully authenticates. - * + * * @author jricher * */ @Component("authenticationTimeStamper") public class AuthenticationTimeStamper extends SavedRequestAwareAuthenticationSuccessHandler { - private static Logger logger = LoggerFactory.getLogger(AuthenticationTimeStamper.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(AuthenticationTimeStamper.class); public static final String AUTH_TIMESTAMP = "AUTH_TIMESTAMP"; @@ -62,9 +66,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo session.setAttribute(AUTH_TIMESTAMP, authTimestamp); - if (session.getAttribute(PromptFilter.PROMPT_REQUESTED) != null) { - session.setAttribute(PromptFilter.PROMPTED, Boolean.TRUE); - session.removeAttribute(PromptFilter.PROMPT_REQUESTED); + if (session.getAttribute(AuthorizationRequestFilter.PROMPT_REQUESTED) != null) { + session.setAttribute(AuthorizationRequestFilter.PROMPTED, Boolean.TRUE); + session.removeAttribute(AuthorizationRequestFilter.PROMPT_REQUESTED); } logger.info("Successful Authentication of " + authentication.getName() + " at " + authTimestamp.toString()); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/BlacklistAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/BlacklistAPI.java index df9584b3ec..6757df8080 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/BlacklistAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/BlacklistAPI.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; @@ -24,10 +25,14 @@ import org.mitre.openid.connect.model.BlacklistedSite; import org.mitre.openid.connect.service.BlacklistedSiteService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -46,14 +51,19 @@ * */ @Controller -@RequestMapping("/api/blacklist") +@RequestMapping("/" + BlacklistAPI.URL) @PreAuthorize("hasRole('ROLE_ADMIN')") public class BlacklistAPI { + public static final String URL = RootController.API_URL + "/blacklist"; + @Autowired private BlacklistedSiteService blacklistService; - private static Logger logger = LoggerFactory.getLogger(BlacklistAPI.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(BlacklistAPI.class); private Gson gson = new Gson(); private JsonParser parser = new JsonParser(); @@ -63,14 +73,14 @@ public class BlacklistAPI { * @param m * @return */ - @RequestMapping(method = RequestMethod.GET, produces = "application/json") + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getAllBlacklistedSites(ModelMap m) { Collection all = blacklistService.getAll(); - m.put("entity", all); + m.put(JsonEntityView.ENTITY, all); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } /** @@ -80,7 +90,7 @@ public String getAllBlacklistedSites(ModelMap m) { * @param p * @return */ - @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String addNewBlacklistedSite(@RequestBody String jsonString, ModelMap m, Principal p) { JsonObject json; @@ -92,29 +102,29 @@ public String addNewBlacklistedSite(@RequestBody String jsonString, ModelMap m, json = parser.parse(jsonString).getAsJsonObject(); blacklist = gson.fromJson(json, BlacklistedSite.class); BlacklistedSite newBlacklist = blacklistService.saveNew(blacklist); - m.put("entity", newBlacklist); + m.put(JsonEntityView.ENTITY, newBlacklist); } catch (JsonSyntaxException e) { logger.error("addNewBlacklistedSite failed due to JsonSyntaxException: ", e); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not save new blacklisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not save new blacklisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } catch (IllegalStateException e) { logger.error("addNewBlacklistedSite failed due to IllegalStateException", e); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not save new blacklisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not save new blacklisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } /** * Update an existing blacklisted site */ - @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json") + @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String updateBlacklistedSite(@PathVariable("id") Long id, @RequestBody String jsonString, ModelMap m, Principal p) { JsonObject json; @@ -129,14 +139,14 @@ public String updateBlacklistedSite(@PathVariable("id") Long id, @RequestBody St } catch (JsonSyntaxException e) { logger.error("updateBlacklistedSite failed due to JsonSyntaxException", e); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not update blacklisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update blacklisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } catch (IllegalStateException e) { logger.error("updateBlacklistedSite failed due to IllegalStateException", e); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not update blacklisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update blacklisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } @@ -144,22 +154,22 @@ public String updateBlacklistedSite(@PathVariable("id") Long id, @RequestBody St if (oldBlacklist == null) { logger.error("updateBlacklistedSite failed; blacklist with id " + id + " could not be found"); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "Could not update blacklisted site. The requested blacklist with id " + id + "could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update blacklisted site. The requested blacklist with id " + id + "could not be found."); + return JsonErrorView.VIEWNAME; } else { BlacklistedSite newBlacklist = blacklistService.update(oldBlacklist, blacklist); - m.put("entity", newBlacklist); + m.put(JsonEntityView.ENTITY, newBlacklist); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } } /** * Delete a blacklisted site - * + * */ @RequestMapping(value="/{id}", method = RequestMethod.DELETE) public String deleteBlacklistedSite(@PathVariable("id") Long id, ModelMap m) { @@ -167,33 +177,34 @@ public String deleteBlacklistedSite(@PathVariable("id") Long id, ModelMap m) { if (blacklist == null) { logger.error("deleteBlacklistedSite failed; blacklist with id " + id + " could not be found"); - m.put("errorMessage", "Could not delete bladklist. The requested bladklist with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(JsonErrorView.ERROR_MESSAGE, "Could not delete bladklist. The requested bladklist with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else { - m.put("code", HttpStatus.OK); + m.put(HttpCodeView.CODE, HttpStatus.OK); blacklistService.remove(blacklist); } - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } /** * Get a single blacklisted site */ - @RequestMapping(value="/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value="/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getBlacklistedSite(@PathVariable("id") Long id, ModelMap m) { BlacklistedSite blacklist = blacklistService.getById(id); if (blacklist == null) { logger.error("getBlacklistedSite failed; blacklist with id " + id + " could not be found"); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "Could not delete bladklist. The requested bladklist with id " + id + " could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not delete bladklist. The requested bladklist with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } else { - m.put("entity", blacklist); + m.put(JsonEntityView.ENTITY, blacklist); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java index 26656f1a3a..45ba59901c 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java @@ -1,39 +1,57 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.lang.reflect.Type; +import java.sql.SQLIntegrityConstraintViolationException; +import java.text.ParseException; import java.util.Collection; -import org.mitre.jose.JWEAlgorithmEmbed; -import org.mitre.jose.JWEEncryptionMethodEmbed; -import org.mitre.jose.JWSAlgorithmEmbed; +import javax.persistence.PersistenceException; + +import org.eclipse.persistence.exceptions.DatabaseException; +import org.mitre.jwt.assertion.AssertionValidator; import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AppType; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; +import org.mitre.oauth2.model.PKCEAlgorithm; import org.mitre.oauth2.service.ClientDetailsEntityService; -import org.mitre.openid.connect.model.UserInfo; -import org.mitre.openid.connect.service.UserInfoService; +import org.mitre.oauth2.web.AuthenticationUtilities; +import org.mitre.openid.connect.exception.ValidationException; +import org.mitre.openid.connect.model.CachedImage; +import org.mitre.openid.connect.service.ClientLogoLoadingService; +import org.mitre.openid.connect.view.ClientEntityViewForAdmins; +import org.mitre.openid.connect.view.ClientEntityViewForUsers; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; @@ -53,76 +71,170 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; +import com.nimbusds.jose.Algorithm; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; + +import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE; +import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME; +import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE; +import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT; +import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG; /** * @author Michael Jett */ @Controller -@RequestMapping("/api/clients") +@RequestMapping("/" + ClientAPI.URL) @PreAuthorize("hasRole('ROLE_USER')") public class ClientAPI { + public static final String URL = RootController.API_URL + "/clients"; + @Autowired private ClientDetailsEntityService clientService; @Autowired - private UserInfoService userInfoService; + private ClientLogoLoadingService clientLogoLoadingService; + + @Autowired + @Qualifier("clientAssertionValidator") + private AssertionValidator assertionValidator; private JsonParser parser = new JsonParser(); private Gson gson = new GsonBuilder() - .serializeNulls() - .registerTypeAdapter(JWSAlgorithmEmbed.class, new JsonDeserializer() { - @Override - public JWSAlgorithmEmbed deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (json.isJsonPrimitive()) { - return JWSAlgorithmEmbed.getForAlgorithmName(json.getAsString()); - } else { - return null; - } - } - }) - .registerTypeAdapter(JWEAlgorithmEmbed.class, new JsonDeserializer() { - @Override - public JWEAlgorithmEmbed deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (json.isJsonPrimitive()) { - return JWEAlgorithmEmbed.getForAlgorithmName(json.getAsString()); - } else { - return null; - } - } - }) - .registerTypeAdapter(JWEEncryptionMethodEmbed.class, new JsonDeserializer() { - @Override - public JWEEncryptionMethodEmbed deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (json.isJsonPrimitive()) { - return JWEEncryptionMethodEmbed.getForAlgorithmName(json.getAsString()); - } else { - return null; - } - } - }) - .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .create(); + .serializeNulls() + .registerTypeAdapter(JWSAlgorithm.class, new JsonDeserializer() { + @Override + public JWSAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + return JWSAlgorithm.parse(json.getAsString()); + } else { + return null; + } + } + }) + .registerTypeAdapter(JWEAlgorithm.class, new JsonDeserializer() { + @Override + public JWEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + return JWEAlgorithm.parse(json.getAsString()); + } else { + return null; + } + } + }) + .registerTypeAdapter(EncryptionMethod.class, new JsonDeserializer() { + @Override + public EncryptionMethod deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + return EncryptionMethod.parse(json.getAsString()); + } else { + return null; + } + } + }) + .registerTypeAdapter(JWKSet.class, new JsonDeserializer() { + @Override + public JWKSet deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonObject()) { + try { + return JWKSet.parse(json.toString()); + } catch (ParseException e) { + return null; + } + } else { + return null; + } + } + }) + .registerTypeAdapter(JWT.class, new JsonDeserializer() { + @Override + public JWT deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + try { + return JWTParser.parse(json.getAsString()); + } catch (ParseException e) { + return null; + } + } else { + return null; + } + } + }) + .registerTypeAdapter(PKCEAlgorithm.class, new JsonDeserializer() { + @Override + public PKCEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + return PKCEAlgorithm.parse(json.getAsString()); + } else { + return null; + } + } + }) + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); - private static Logger logger = LoggerFactory.getLogger(ClientAPI.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ClientAPI.class); /** * Get a list of all clients * @param modelAndView * @return */ - @RequestMapping(method = RequestMethod.GET, produces = "application/json") + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String apiGetAllClients(Model model, Authentication auth) { Collection clients = clientService.getAllClients(); - model.addAttribute("entity", clients); + model.addAttribute(JsonEntityView.ENTITY, clients); - if (isAdmin(auth)) { - return "clientEntityViewAdmins"; + if (AuthenticationUtilities.isAdmin(auth)) { + return ClientEntityViewForAdmins.VIEWNAME; } else { - return "clientEntityViewUsers"; + return ClientEntityViewForUsers.VIEWNAME; } } @@ -134,7 +246,7 @@ public String apiGetAllClients(Model model, Authentication auth) { * @return */ @PreAuthorize("hasRole('ROLE_ADMIN')") - @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String apiAddClient(@RequestBody String jsonString, Model m, Authentication auth) { JsonObject json = null; @@ -143,71 +255,95 @@ public String apiAddClient(@RequestBody String jsonString, Model m, Authenticati try { json = parser.parse(jsonString).getAsJsonObject(); client = gson.fromJson(json, ClientDetailsEntity.class); - } - catch (JsonSyntaxException e) { + client = validateSoftwareStatement(client); + } catch (JsonSyntaxException e) { logger.error("apiAddClient failed due to JsonSyntaxException", e); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Could not save new client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } catch (IllegalStateException e) { logger.error("apiAddClient failed due to IllegalStateException", e); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Could not save new client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; + } catch (ValidationException e) { + logger.error("apiUpdateClient failed due to ValidationException", e); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException."); + return JsonErrorView.VIEWNAME; } // if they leave the client identifier empty, force it to be generated if (Strings.isNullOrEmpty(client.getClientId())) { client = clientService.generateClientId(client); - } - - if (client.getTokenEndpointAuthMethod() == null || + } + + if (client.getTokenEndpointAuthMethod() == null || client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) { // we shouldn't have a secret for this client - + client.setClientSecret(null); - - } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) - || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) + + } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) + || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) { - + // if they've asked for us to generate a client secret (or they left it blank but require one), do so here - if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean() + if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean() || Strings.isNullOrEmpty(client.getClientSecret())) { client = clientService.generateClientSecret(client); } } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { - if (Strings.isNullOrEmpty(client.getJwksUri())) { + if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) { logger.error("tried to create client with private key auth but no private key"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Can not create a client with private key authentication without registering a key via the JWS Set URI."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWK Set URI or JWK Set Value."); + return JsonErrorView.VIEWNAME; } - + // otherwise we shouldn't have a secret for this client client.setClientSecret(null); - + } else { - + logger.error("unknown auth method"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Unknown auth method requested"); - return "jsonErrorView"; - - + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested"); + return JsonErrorView.VIEWNAME; + + } client.setDynamicallyRegistered(false); - ClientDetailsEntity newClient = clientService.saveNewClient(client); - m.addAttribute("entity", newClient); + try { + ClientDetailsEntity newClient = clientService.saveNewClient(client); + m.addAttribute(JsonEntityView.ENTITY, newClient); - if (isAdmin(auth)) { - return "clientEntityViewAdmins"; - } else { - return "clientEntityViewUsers"; + if (AuthenticationUtilities.isAdmin(auth)) { + return ClientEntityViewForAdmins.VIEWNAME; + } else { + return ClientEntityViewForUsers.VIEWNAME; + } + } catch (IllegalArgumentException e) { + logger.error("Unable to save client: {}", e.getMessage()); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client: " + e.getMessage()); + return JsonErrorView.VIEWNAME; + } catch (PersistenceException e) { + Throwable cause = e.getCause(); + if (cause instanceof DatabaseException) { + Throwable databaseExceptionCause = cause.getCause(); + if(databaseExceptionCause instanceof SQLIntegrityConstraintViolationException) { + logger.error("apiAddClient failed; duplicate client id entry found: {}", client.getClientId()); + m.addAttribute(HttpCodeView.CODE, HttpStatus.CONFLICT); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client. Duplicate client id entry found: " + client.getClientId()); + return JsonErrorView.VIEWNAME; + } + } + throw e; } } @@ -220,7 +356,7 @@ public String apiAddClient(@RequestBody String jsonString, Model m, Authenticati * @return */ @PreAuthorize("hasRole('ROLE_ADMIN')") - @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json") + @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String apiUpdateClient(@PathVariable("id") Long id, @RequestBody String jsonString, Model m, Authentication auth) { JsonObject json = null; @@ -230,26 +366,31 @@ public String apiUpdateClient(@PathVariable("id") Long id, @RequestBody String j // parse the client passed in (from JSON) and fetch the old client from the store json = parser.parse(jsonString).getAsJsonObject(); client = gson.fromJson(json, ClientDetailsEntity.class); - } - catch (JsonSyntaxException e) { + client = validateSoftwareStatement(client); + } catch (JsonSyntaxException e) { logger.error("apiUpdateClient failed due to JsonSyntaxException", e); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Could not update client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } catch (IllegalStateException e) { logger.error("apiUpdateClient failed due to IllegalStateException", e); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Could not update client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; + } catch (ValidationException e) { + logger.error("apiUpdateClient failed due to ValidationException", e); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException."); + return JsonErrorView.VIEWNAME; } ClientDetailsEntity oldClient = clientService.getClientById(id); if (oldClient == null) { logger.error("apiUpdateClient failed; client with id " + id + " could not be found."); - m.addAttribute("code", HttpStatus.NOT_FOUND); - m.addAttribute("errorMessage", "Could not update client. The requested client with id " + id + "could not be found."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The requested client with id " + id + "could not be found."); + return JsonErrorView.VIEWNAME; } // if they leave the client identifier empty, force it to be generated @@ -260,48 +401,55 @@ public String apiUpdateClient(@PathVariable("id") Long id, @RequestBody String j if (client.getTokenEndpointAuthMethod() == null || client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) { // we shouldn't have a secret for this client - + client.setClientSecret(null); - - } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) - || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) + + } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) + || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) { - + // if they've asked for us to generate a client secret (or they left it blank but require one), do so here - if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean() + if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean() || Strings.isNullOrEmpty(client.getClientSecret())) { client = clientService.generateClientSecret(client); } } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { - if (Strings.isNullOrEmpty(client.getJwksUri())) { + if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) { logger.error("tried to create client with private key auth but no private key"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Can not create a client with private key authentication without registering a key via the JWS Set URI."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWK Set URI or JWK Set Value."); + return JsonErrorView.VIEWNAME; } - + // otherwise we shouldn't have a secret for this client client.setClientSecret(null); - + } else { - + logger.error("unknown auth method"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Unknown auth method requested"); - return "jsonErrorView"; - - + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested"); + return JsonErrorView.VIEWNAME; + + } - ClientDetailsEntity newClient = clientService.updateClient(oldClient, client); - m.addAttribute("entity", newClient); + try { + ClientDetailsEntity newClient = clientService.updateClient(oldClient, client); + m.addAttribute(JsonEntityView.ENTITY, newClient); - if (isAdmin(auth)) { - return "clientEntityViewAdmins"; - } else { - return "clientEntityViewUsers"; + if (AuthenticationUtilities.isAdmin(auth)) { + return ClientEntityViewForAdmins.VIEWNAME; + } else { + return ClientEntityViewForUsers.VIEWNAME; + } + } catch (IllegalArgumentException e) { + logger.error("Unable to save client: {}", e.getMessage()); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client: " + e.getMessage()); + return JsonErrorView.VIEWNAME; } } @@ -319,15 +467,15 @@ public String apiDeleteClient(@PathVariable("id") Long id, ModelAndView modelAnd if (client == null) { logger.error("apiDeleteClient failed; client with id " + id + " could not be found."); - modelAndView.getModelMap().put("code", HttpStatus.NOT_FOUND); - modelAndView.getModelMap().put("errorMessage", "Could not delete client. The requested client with id " + id + "could not be found."); - return "jsonErrorView"; + modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + modelAndView.getModelMap().put(JsonErrorView.ERROR_MESSAGE, "Could not delete client. The requested client with id " + id + "could not be found."); + return JsonErrorView.VIEWNAME; } else { - modelAndView.getModelMap().put("code", HttpStatus.OK); + modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.OK); clientService.deleteClient(client); } - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } @@ -337,38 +485,188 @@ public String apiDeleteClient(@PathVariable("id") Long id, ModelAndView modelAnd * @param modelAndView * @return */ - @RequestMapping(value="/{id}", method=RequestMethod.GET, produces = "application/json") + @RequestMapping(value="/{id}", method=RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String apiShowClient(@PathVariable("id") Long id, Model model, Authentication auth) { ClientDetailsEntity client = clientService.getClientById(id); if (client == null) { logger.error("apiShowClient failed; client with id " + id + " could not be found."); - model.addAttribute("code", HttpStatus.NOT_FOUND); - model.addAttribute("errorMessage", "The requested client with id " + id + " could not be found."); - return "jsonErrorView"; + model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + model.addAttribute(JsonErrorView.ERROR_MESSAGE, "The requested client with id " + id + " could not be found."); + return JsonErrorView.VIEWNAME; } - model.addAttribute("entity", client); + model.addAttribute(JsonEntityView.ENTITY, client); - if (isAdmin(auth)) { - return "clientEntityViewAdmins"; + if (AuthenticationUtilities.isAdmin(auth)) { + return ClientEntityViewForAdmins.VIEWNAME; } else { - return "clientEntityViewUsers"; + return ClientEntityViewForUsers.VIEWNAME; } } /** - * Check to see if the given auth object has ROLE_ADMIN assigned to it or not - * @param auth - * @return + * Get the logo image for a client + * @param id */ - private boolean isAdmin(Authentication auth) { - for (GrantedAuthority grantedAuthority : auth.getAuthorities()) { - if (grantedAuthority.getAuthority().equals("ROLE_ADMIN")) { - return true; + @RequestMapping(value = "/{id}/logo", method=RequestMethod.GET, produces = { MediaType.IMAGE_GIF_VALUE, MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE }) + public ResponseEntity getClientLogo(@PathVariable("id") Long id, Model model) { + + ClientDetailsEntity client = clientService.getClientById(id); + + if (client == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } else if (Strings.isNullOrEmpty(client.getLogoUri())) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } else { + // get the image from cache + CachedImage image = clientLogoLoadingService.getLogo(client); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(image.getContentType())); + headers.setContentLength(image.getLength()); + + return new ResponseEntity<>(image.getData(), headers, HttpStatus.OK); + } + } + + private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException { + if (newClient.getSoftwareStatement() != null) { + if (assertionValidator.isValid(newClient.getSoftwareStatement())) { + // we have a software statement and its envelope passed all the checks from our validator + + // swap out all of the client's fields for the associated parts of the software statement + try { + JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet(); + for (String claim : claimSet.getClaims().keySet()) { + switch (claim) { + case SOFTWARE_STATEMENT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST); + case CLAIMS_REDIRECT_URIS: + newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case CLIENT_SECRET_EXPIRES_AT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST); + case CLIENT_ID_ISSUED_AT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST); + case REGISTRATION_CLIENT_URI: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST); + case REGISTRATION_ACCESS_TOKEN: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST); + case REQUEST_URIS: + newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case POST_LOGOUT_REDIRECT_URIS: + newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case INITIATE_LOGIN_URI: + newClient.setInitiateLoginUri(claimSet.getStringClaim(claim)); + break; + case DEFAULT_ACR_VALUES: + newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case REQUIRE_AUTH_TIME: + newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim)); + break; + case DEFAULT_MAX_AGE: + newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim)); + break; + case TOKEN_ENDPOINT_AUTH_SIGNING_ALG: + newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_ENCRYPTED_RESPONSE_ENC: + newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_ENCRYPTED_RESPONSE_ALG: + newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_SIGNED_RESPONSE_ALG: + newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_ENCRYPTED_RESPONSE_ENC: + newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_ENCRYPTED_RESPONSE_ALG: + newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_SIGNED_RESPONSE_ALG: + newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case REQUEST_OBJECT_SIGNING_ALG: + newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case SUBJECT_TYPE: + newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim))); + break; + case SECTOR_IDENTIFIER_URI: + newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim)); + break; + case APPLICATION_TYPE: + newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim))); + break; + case JWKS_URI: + newClient.setJwksUri(claimSet.getStringClaim(claim)); + break; + case JWKS: + newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString())); + break; + case POLICY_URI: + newClient.setPolicyUri(claimSet.getStringClaim(claim)); + break; + case RESPONSE_TYPES: + newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case GRANT_TYPES: + newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case SCOPE: + newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim))); + break; + case TOKEN_ENDPOINT_AUTH_METHOD: + newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim))); + break; + case TOS_URI: + newClient.setTosUri(claimSet.getStringClaim(claim)); + break; + case CONTACTS: + newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case LOGO_URI: + newClient.setLogoUri(claimSet.getStringClaim(claim)); + break; + case CLIENT_URI: + newClient.setClientUri(claimSet.getStringClaim(claim)); + break; + case CLIENT_NAME: + newClient.setClientName(claimSet.getStringClaim(claim)); + break; + case REDIRECT_URIS: + newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case CLIENT_SECRET: + throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST); + case CLIENT_ID: + throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST); + + default: + logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim)); + break; + } + } + + return newClient; + } catch (ParseException e) { + throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST); + } + } else { + throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST); } + } else { + // nothing to see here, carry on + return newClient; } - return false; + } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DataAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DataAPI.java new file mode 100644 index 0000000000..84b0de503b --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DataAPI.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.web; + +import java.io.IOException; +import java.io.Reader; +import java.security.Principal; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mitre.openid.connect.service.impl.MITREidDataService_1_3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.google.common.collect.ImmutableList; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * API endpoint for importing and exporting the current state of a server. + * Includes all tokens, grants, whitelists, blacklists, and clients. + * + * @author jricher + * + */ +@Controller +@RequestMapping("/" + DataAPI.URL) +@PreAuthorize("hasRole('ROLE_ADMIN')") // you need to be an admin to even think about this -- this is a potentially dangerous API!! +public class DataAPI { + + public static final String URL = RootController.API_URL + "/data"; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DataAPI.class); + + private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + + @Autowired + private ConfigurationPropertiesBean config; + + @Autowired + private List importers; + + private List supportedVersions = ImmutableList.of( + MITREidDataService.MITREID_CONNECT_1_0, + MITREidDataService.MITREID_CONNECT_1_1, + MITREidDataService.MITREID_CONNECT_1_2, + MITREidDataService.MITREID_CONNECT_1_3); + + @Autowired + private MITREidDataService_1_3 exporter; + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public String importData(Reader in, Model m) throws IOException { + + JsonReader reader = new JsonReader(in); + + reader.beginObject(); + + while (reader.hasNext()) { + JsonToken tok = reader.peek(); + switch (tok) { + case NAME: + String name = reader.nextName(); + + if (supportedVersions.contains(name)) { + // we're working with a known data version tag + for (MITREidDataService dataService : importers) { + // dispatch to the correct service + if (dataService.supportsVersion(name)) { + dataService.importData(reader); + break; + } + } + } else { + // consume the next bit silently for now + logger.debug("Skipping value for " + name); // TODO: write these out? + reader.skipValue(); + } + break; + case END_OBJECT: + break; + case END_DOCUMENT: + break; + } + } + + reader.endObject(); + + return "httpCodeView"; + } + + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public void exportData(HttpServletResponse resp, Principal prin) throws IOException { + + resp.setContentType(MediaType.APPLICATION_JSON_VALUE); + + // this writer puts things out onto the wire + JsonWriter writer = new JsonWriter(resp.getWriter()); + writer.setIndent(" "); + + try { + + writer.beginObject(); + + writer.name("exported-at"); + writer.value(dateFormat.format(new Date())); + + writer.name("exported-from"); + writer.value(config.getIssuer()); + + writer.name("exported-by"); + writer.value(prin.getName()); + + // delegate to the service to do the actual export + exporter.exportData(writer); + + writer.endObject(); // end root + writer.close(); + + } catch (IOException e) { + logger.error("Unable to export data", e); + } + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java similarity index 51% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java index b5766e8902..79ea68116c 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.io.UnsupportedEncodingException; @@ -23,9 +24,11 @@ import java.util.Set; import java.util.concurrent.TimeUnit; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.assertion.AssertionValidator; import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AppType; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.RegisteredClient; import org.mitre.oauth2.model.SystemScope; @@ -37,11 +40,17 @@ import org.mitre.openid.connect.exception.ValidationException; import org.mitre.openid.connect.service.BlacklistedSiteService; import org.mitre.openid.connect.service.OIDCTokenService; +import org.mitre.openid.connect.view.ClientInformationResponseView; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonErrorView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; import org.springframework.stereotype.Controller; @@ -56,10 +65,56 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.gson.JsonSyntaxException; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWTClaimsSet; + +import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE; +import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME; +import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE; +import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT; +import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG; @Controller -@RequestMapping(value = "register") -public class ClientDynamicRegistrationEndpoint { +@RequestMapping(value = DynamicClientRegistrationEndpoint.URL) +public class DynamicClientRegistrationEndpoint { + + public static final String URL = "register"; @Autowired private ClientDetailsEntityService clientService; @@ -67,9 +122,6 @@ public class ClientDynamicRegistrationEndpoint { @Autowired private OAuth2TokenEntityService tokenService; - @Autowired - private JwtSigningAndValidationService jwtService; - @Autowired private SystemScopeService scopeService; @@ -82,7 +134,14 @@ public class ClientDynamicRegistrationEndpoint { @Autowired private OIDCTokenService connectTokenService; - private static Logger logger = LoggerFactory.getLogger(ClientDynamicRegistrationEndpoint.class); + @Autowired + @Qualifier("clientAssertionValidator") + private AssertionValidator assertionValidator; + + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(DynamicClientRegistrationEndpoint.class); /** * Create a new Client, issue a client ID, and create a registration access token. @@ -91,7 +150,7 @@ public class ClientDynamicRegistrationEndpoint { * @param p * @return */ - @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String registerNewClient(@RequestBody String jsonString, Model m) { ClientDetailsEntity newClient = null; @@ -101,8 +160,8 @@ public String registerNewClient(@RequestBody String jsonString, Model m) { // bad parse // didn't parse, this is a bad request logger.error("registerNewClient failed; submitted JSON is malformed"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - return "httpCodeView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + return HttpCodeView.VIEWNAME; } if (newClient != null) { @@ -118,6 +177,7 @@ public String registerNewClient(@RequestBody String jsonString, Model m) { // do validation on the fields try { + newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values newClient = validateScopes(newClient); newClient = validateResponseTypes(newClient); newClient = validateGrantTypes(newClient); @@ -125,12 +185,12 @@ public String registerNewClient(@RequestBody String jsonString, Model m) { newClient = validateAuth(newClient); } catch (ValidationException ve) { // validation failed, return an error - m.addAttribute("error", ve.getError()); - m.addAttribute("errorMessage", ve.getErrorDescription()); - m.addAttribute("code", ve.getStatus()); - return "jsonErrorView"; + m.addAttribute(JsonErrorView.ERROR, ve.getError()); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); + m.addAttribute(HttpCodeView.CODE, ve.getStatus()); + return JsonErrorView.VIEWNAME; } - + if (newClient.getTokenEndpointAuthMethod() == null) { newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); } @@ -144,9 +204,26 @@ public String registerNewClient(@RequestBody String jsonString, Model m) { } // set some defaults for token timeouts - newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr - newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min - newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked + if (config.isHeartMode()) { + // heart mode has different defaults depending on primary grant type + if (newClient.getGrantTypes().contains("authorization_code")) { + newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr + newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min + newClient.setRefreshTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(24)); // refresh tokens good for 24hr + } else if (newClient.getGrantTypes().contains("implicit")) { + newClient.setAccessTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(15)); // access tokens good for 15min + newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min + newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens + } else if (newClient.getGrantTypes().contains("client_credentials")) { + newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(6)); // access tokens good for 6hr + newClient.setIdTokenValiditySeconds(0); // no id tokens + newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens + } + } else { + newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr + newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min + newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked + } // this client has been dynamically registered (obviously) newClient.setDynamicallyRegistered(true); @@ -160,34 +237,30 @@ public String registerNewClient(@RequestBody String jsonString, Model m) { // generate the registration access token OAuth2AccessTokenEntity token = connectTokenService.createRegistrationAccessToken(savedClient); - tokenService.saveAccessToken(token); + token = tokenService.saveAccessToken(token); // send it all out to the view RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); m.addAttribute("client", registered); - m.addAttribute("code", HttpStatus.CREATED); // http 201 + m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201 - return "clientInformationResponseView"; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR); - return "httpCodeView"; + return ClientInformationResponseView.VIEWNAME; } catch (IllegalArgumentException e) { logger.error("Couldn't save client", e); - - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Unable to save client due to invalid or inconsistent metadata."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - - return "jsonErrorView"; + + m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + + return JsonErrorView.VIEWNAME; } } else { // didn't parse, this is a bad request logger.error("registerNewClient failed; submitted JSON is malformed"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } @@ -200,35 +273,29 @@ public String registerNewClient(@RequestBody String jsonString, Model m) { * @return */ @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String readClientConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { ClientDetailsEntity client = clientService.loadClientByClientId(clientId); if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { - try { - OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client); - RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); + OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, client); + RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); - // send it all out to the view - m.addAttribute("client", registered); - m.addAttribute("code", HttpStatus.OK); // http 200 + // send it all out to the view + m.addAttribute("client", registered); + m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 + + return ClientInformationResponseView.VIEWNAME; - return "clientInformationResponseView"; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR); - return "httpCodeView"; - } - } else { // client mismatch logger.error("readClientConfiguration failed, client ID mismatch: " + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } @@ -241,7 +308,7 @@ public String readClientConfiguration(@PathVariable("id") String clientId, Model * @return */ @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = "application/json", consumes = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public String updateClient(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) { @@ -252,8 +319,8 @@ public String updateClient(@PathVariable("id") String clientId, @RequestBody Str // bad parse // didn't parse, this is a bad request logger.error("updateClient failed; submitted JSON is malformed"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - return "httpCodeView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + return HttpCodeView.VIEWNAME; } ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); @@ -278,6 +345,7 @@ public String updateClient(@PathVariable("id") String clientId, @RequestBody Str // do validation on the fields try { + newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values newClient = validateScopes(newClient); newClient = validateResponseTypes(newClient); newClient = validateGrantTypes(newClient); @@ -285,45 +353,41 @@ public String updateClient(@PathVariable("id") String clientId, @RequestBody Str newClient = validateAuth(newClient); } catch (ValidationException ve) { // validation failed, return an error - m.addAttribute("error", ve.getError()); - m.addAttribute("errorMessage", ve.getErrorDescription()); - m.addAttribute("code", ve.getStatus()); - return "jsonErrorView"; + m.addAttribute(JsonErrorView.ERROR, ve.getError()); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); + m.addAttribute(HttpCodeView.CODE, ve.getStatus()); + return JsonErrorView.VIEWNAME; } - + try { // save the client ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); - OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, savedClient); + OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, savedClient); RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); // send it all out to the view m.addAttribute("client", registered); - m.addAttribute("code", HttpStatus.OK); // http 200 + m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 - return "clientInformationResponseView"; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR); - return "httpCodeView"; + return ClientInformationResponseView.VIEWNAME; } catch (IllegalArgumentException e) { logger.error("Couldn't save client", e); - - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Unable to save client due to invalid or inconsistent metadata."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - - return "jsonErrorView"; + + m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + + return JsonErrorView.VIEWNAME; } } else { // client mismatch logger.error("updateClient failed, client ID mismatch: " + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } @@ -335,7 +399,7 @@ public String updateClient(@PathVariable("id") String clientId, @RequestBody Str * @return */ @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) public String deleteClient(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { ClientDetailsEntity client = clientService.loadClientByClientId(clientId); @@ -344,28 +408,25 @@ public String deleteClient(@PathVariable("id") String clientId, Model m, OAuth2A clientService.deleteClient(client); - m.addAttribute("code", HttpStatus.NO_CONTENT); // http 204 + m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } else { // client mismatch logger.error("readClientConfiguration failed, client ID mismatch: " + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException { - // set of scopes that are OK for clients to dynamically register for - Set dynScopes = scopeService.getDynReg(); - // scopes that the client is asking for Set requestedScopes = scopeService.fromStrings(newClient.getScope()); // the scopes that the client can have must be a subset of the dynamically allowed scopes - Set allowedScopes = Sets.intersection(dynScopes, requestedScopes); + Set allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes); // if the client didn't ask for any, give them the defaults if (allowedScopes == null || allowedScopes.isEmpty()) { @@ -373,17 +434,17 @@ private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws } newClient.setScope(scopeService.toStrings(allowedScopes)); - + return newClient; } - + private ClientDetailsEntity validateResponseTypes(ClientDetailsEntity newClient) throws ValidationException { if (newClient.getResponseTypes() == null) { newClient.setResponseTypes(new HashSet()); } return newClient; } - + private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient) throws ValidationException { // set default grant types if needed if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) { @@ -392,16 +453,21 @@ private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient) th } else { newClient.setGrantTypes(Sets.newHashSet("authorization_code")); // allow authorization code grant type by default } + if (config.isDualClient()) { + Set extendedGrandTypes = newClient.getGrantTypes(); + extendedGrandTypes.add("client_credentials"); + newClient.setGrantTypes(extendedGrandTypes); + } } - + // filter out unknown grant types // TODO: make this a pluggable service - Set requestedGrantTypes = new HashSet(newClient.getGrantTypes()); + Set requestedGrantTypes = new HashSet<>(newClient.getGrantTypes()); requestedGrantTypes.retainAll( - ImmutableSet.of("authorization_code", "implicit", - "password", "client_credentials", "refresh_token", - "urn:ietf:params:oauth:grant_type:redelegate")); - + ImmutableSet.of("authorization_code", "implicit", + "password", "client_credentials", "refresh_token", + "urn:ietf:params:oauth:grant_type:redelegate")); + // don't allow "password" grant type for dynamic registration if (newClient.getGrantTypes().contains("password")) { // return an error, you can't dynamically register for the password grant @@ -413,7 +479,7 @@ private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient) th // check for incompatible grants if (newClient.getGrantTypes().contains("implicit") || - newClient.getGrantTypes().contains("client_credentials")) { + (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) { // return an error, you can't have these grant types together throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); } @@ -422,60 +488,58 @@ private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient) th // return an error, you can't have this grant type and response type together throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); } - + newClient.getResponseTypes().add("code"); - - } - + if (newClient.getGrantTypes().contains("implicit")) { // check for incompatible grants if (newClient.getGrantTypes().contains("authorization_code") || - newClient.getGrantTypes().contains("client_credentials")) { + (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) { // return an error, you can't have these grant types together throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); } - + if (newClient.getResponseTypes().contains("code")) { // return an error, you can't have this grant type and response type together throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); } - + newClient.getResponseTypes().add("token"); - + // don't allow refresh tokens in implicit clients newClient.getGrantTypes().remove("refresh_token"); - newClient.getScope().remove("offline_access"); + newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS); } - + if (newClient.getGrantTypes().contains("client_credentials")) { // check for incompatible grants - if (newClient.getGrantTypes().contains("authorization_code") || - newClient.getGrantTypes().contains("implicit")) { + if (!config.isDualClient() && + (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit"))) { // return an error, you can't have these grant types together throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); } - + if (!newClient.getResponseTypes().isEmpty()) { // return an error, you can't have this grant type and response type together throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); } - + // don't allow refresh tokens or id tokens in client_credentials clients newClient.getGrantTypes().remove("refresh_token"); - newClient.getScope().remove("offline_access"); - newClient.getScope().remove("openid"); + newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS); + newClient.getScope().remove(SystemScopeService.OPENID_SCOPE); } - + if (newClient.getGrantTypes().isEmpty()) { // return an error, you need at least one grant type selected throw new ValidationException("invalid_client_metadata", "Clients must register at least one grant type.", HttpStatus.BAD_REQUEST); - } + } return newClient; } - + private ClientDetailsEntity validateRedirectUris(ClientDetailsEntity newClient) throws ValidationException { // check to make sure this client registered a redirect URI if using a redirect flow if (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit")) { @@ -489,17 +553,17 @@ private ClientDetailsEntity validateRedirectUris(ClientDetailsEntity newClient) // return an error throw new ValidationException("invalid_redirect_uri", "Redirect URI is not allowed: " + uri, HttpStatus.BAD_REQUEST); } - - if (uri.contains("#")) { + + if (uri.contains("#")) { // if it contains the hash symbol then it has a fragment, which isn't allowed throw new ValidationException("invalid_redirect_uri", "Redirect URI can not have a fragment", HttpStatus.BAD_REQUEST); } } } - + return newClient; } - + private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws ValidationException { if (newClient.getTokenEndpointAuthMethod() == null) { newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); @@ -514,10 +578,10 @@ private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws V newClient = clientService.generateClientSecret(newClient); } } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { - if (Strings.isNullOrEmpty(newClient.getJwksUri())) { + if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); } - + newClient.setClientSecret(null); } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) { newClient.setClientSecret(null); @@ -526,14 +590,162 @@ private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws V } return newClient; } - - private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication auth, ClientDetailsEntity client) { - + + + /** + * @param newClient + * @return + * @throws ValidationException + */ + private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException { + if (newClient.getSoftwareStatement() != null) { + if (assertionValidator.isValid(newClient.getSoftwareStatement())) { + // we have a software statement and its envelope passed all the checks from our validator + + // swap out all of the client's fields for the associated parts of the software statement + try { + JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet(); + for (String claim : claimSet.getClaims().keySet()) { + switch (claim) { + case SOFTWARE_STATEMENT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST); + case CLAIMS_REDIRECT_URIS: + newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case CLIENT_SECRET_EXPIRES_AT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST); + case CLIENT_ID_ISSUED_AT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST); + case REGISTRATION_CLIENT_URI: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST); + case REGISTRATION_ACCESS_TOKEN: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST); + case REQUEST_URIS: + newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case POST_LOGOUT_REDIRECT_URIS: + newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case INITIATE_LOGIN_URI: + newClient.setInitiateLoginUri(claimSet.getStringClaim(claim)); + break; + case DEFAULT_ACR_VALUES: + newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case REQUIRE_AUTH_TIME: + newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim)); + break; + case DEFAULT_MAX_AGE: + newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim)); + break; + case TOKEN_ENDPOINT_AUTH_SIGNING_ALG: + newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_ENCRYPTED_RESPONSE_ENC: + newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_ENCRYPTED_RESPONSE_ALG: + newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_SIGNED_RESPONSE_ALG: + newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_ENCRYPTED_RESPONSE_ENC: + newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_ENCRYPTED_RESPONSE_ALG: + newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_SIGNED_RESPONSE_ALG: + newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case REQUEST_OBJECT_SIGNING_ALG: + newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case SUBJECT_TYPE: + newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim))); + break; + case SECTOR_IDENTIFIER_URI: + newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim)); + break; + case APPLICATION_TYPE: + newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim))); + break; + case JWKS_URI: + newClient.setJwksUri(claimSet.getStringClaim(claim)); + break; + case JWKS: + newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString())); + break; + case POLICY_URI: + newClient.setPolicyUri(claimSet.getStringClaim(claim)); + break; + case RESPONSE_TYPES: + newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case GRANT_TYPES: + newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case SCOPE: + newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim))); + break; + case TOKEN_ENDPOINT_AUTH_METHOD: + newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim))); + break; + case TOS_URI: + newClient.setTosUri(claimSet.getStringClaim(claim)); + break; + case CONTACTS: + newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case LOGO_URI: + newClient.setLogoUri(claimSet.getStringClaim(claim)); + break; + case CLIENT_URI: + newClient.setClientUri(claimSet.getStringClaim(claim)); + break; + case CLIENT_NAME: + newClient.setClientName(claimSet.getStringClaim(claim)); + break; + case REDIRECT_URIS: + newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case CLIENT_SECRET: + throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST); + case CLIENT_ID: + throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST); + + default: + logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim)); + break; + } + } + + return newClient; + } catch (ParseException e) { + throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST); + } + } else { + throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST); + } + } else { + // nothing to see here, carry on + return newClient; + } + + } + + + /* + * Rotates the registration token if it's expired, otherwise returns it + */ + private OAuth2AccessTokenEntity rotateRegistrationTokenIfNecessary(OAuth2Authentication auth, ClientDetailsEntity client) { + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); - + if (config.getRegTokenLifeTime() != null) { - + try { // Re-issue the token if it has been issued before [currentTime - validity] Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000); @@ -556,4 +768,5 @@ private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication return token; } } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/EndSessionEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/EndSessionEndpoint.java new file mode 100644 index 0000000000..26055501a4 --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/EndSessionEndpoint.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.web; + +import java.text.ParseException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.mitre.jwt.assertion.AssertionValidator; +import org.mitre.jwt.assertion.impl.SelfAssertionValidator; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.model.UserInfo; +import org.mitre.openid.connect.service.UserInfoService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; + +/** + * Implementation of the End Session Endpoint from OIDC session management + * + * @author jricher + * + */ +@Controller +public class EndSessionEndpoint { + + public static final String URL = "endsession"; + + private static final String CLIENT_KEY = "client"; + private static final String STATE_KEY = "state"; + private static final String REDIRECT_URI_KEY = "redirectUri"; + + private static Logger logger = LoggerFactory.getLogger(EndSessionEndpoint.class); + + @Autowired + private SelfAssertionValidator validator; + + @Autowired + private UserInfoService userInfoService; + + @Autowired + private ClientDetailsEntityService clientService; + + @RequestMapping(value = "/" + URL, method = RequestMethod.GET) + public String endSession(@RequestParam (value = "id_token_hint", required = false) String idTokenHint, + @RequestParam (value = "post_logout_redirect_uri", required = false) String postLogoutRedirectUri, + @RequestParam (value = STATE_KEY, required = false) String state, + HttpServletRequest request, + HttpServletResponse response, + HttpSession session, + Authentication auth, Model m) { + + // conditionally filled variables + JWTClaimsSet idTokenClaims = null; // pulled from the parsed and validated ID token + ClientDetailsEntity client = null; // pulled from ID token's audience field + + if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) { + session.setAttribute(REDIRECT_URI_KEY, postLogoutRedirectUri); + } + if (!Strings.isNullOrEmpty(state)) { + session.setAttribute(STATE_KEY, state); + } + + // parse the ID token hint to see if it's valid + if (!Strings.isNullOrEmpty(idTokenHint)) { + try { + JWT idToken = JWTParser.parse(idTokenHint); + + if (validator.isValid(idToken)) { + // we issued this ID token, figure out who it's for + idTokenClaims = idToken.getJWTClaimsSet(); + + String clientId = Iterables.getOnlyElement(idTokenClaims.getAudience()); + + client = clientService.loadClientByClientId(clientId); + + // save a reference in the session for us to pick up later + //session.setAttribute("endSession_idTokenHint_claims", idTokenClaims); + session.setAttribute(CLIENT_KEY, client); + } + } catch (ParseException e) { + // it's not a valid ID token, ignore it + logger.debug("Invalid id token hint", e); + } catch (InvalidClientException e) { + // couldn't find the client, ignore it + logger.debug("Invalid client", e); + } + } + + // are we logged in or not? + if (auth == null || !request.isUserInRole("ROLE_USER")) { + // we're not logged in anyway, process the final redirect bits if needed + return processLogout(null, request, response, session, auth, m); + } else { + // we are logged in, need to prompt the user before we log out + + // see who the current user is + UserInfo ui = userInfoService.getByUsername(auth.getName()); + + if (idTokenClaims != null) { + String subject = idTokenClaims.getSubject(); + // see if the current user is the same as the one in the ID token + // TODO: should we do anything different in these cases? + if (!Strings.isNullOrEmpty(subject) && subject.equals(ui.getSub())) { + // it's the same user + } else { + // it's not the same user + } + } + + m.addAttribute("client", client); + m.addAttribute("idToken", idTokenClaims); + + // display the log out confirmation page + return "logoutConfirmation"; + } + } + + @RequestMapping(value = "/" + URL, method = RequestMethod.POST) + public String processLogout(@RequestParam(value = "approve", required = false) String approved, + HttpServletRequest request, + HttpServletResponse response, + HttpSession session, + Authentication auth, Model m) { + + String redirectUri = (String) session.getAttribute(REDIRECT_URI_KEY); + String state = (String) session.getAttribute(STATE_KEY); + ClientDetailsEntity client = (ClientDetailsEntity) session.getAttribute(CLIENT_KEY); + + if (!Strings.isNullOrEmpty(approved)) { + // use approved, perform the logout + if (auth != null){ + new SecurityContextLogoutHandler().logout(request, response, auth); + } + SecurityContextHolder.getContext().setAuthentication(null); + // TODO: hook into other logout post-processing + } + + // if the user didn't approve, don't log out but hit the landing page anyway for redirect as needed + + + + // if we have a client AND the client has post-logout redirect URIs + // registered AND the URI given is in that list, then... + if (!Strings.isNullOrEmpty(redirectUri) && + client != null && client.getPostLogoutRedirectUris() != null) { + + if (client.getPostLogoutRedirectUris().contains(redirectUri)) { + // TODO: future, add the redirect URI to the model for the display page for an interstitial + // m.addAttribute("redirectUri", postLogoutRedirectUri); + + UriComponents uri = UriComponentsBuilder.fromHttpUrl(redirectUri).queryParam("state", state).build(); + + return "redirect:" + uri; + } + } + + // otherwise, return to a nice post-logout landing page + return "postLogout"; + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/JsonWebKeyEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/JWKSetPublishingEndpoint.java similarity index 68% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/web/JsonWebKeyEndpoint.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/web/JWKSetPublishingEndpoint.java index 623e1edc17..0c102a5494 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/JsonWebKeyEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/JWKSetPublishingEndpoint.java @@ -1,25 +1,28 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.util.Map; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.openid.connect.view.JWKSetView; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,12 +30,14 @@ import com.nimbusds.jose.jwk.JWK; @Controller -public class JsonWebKeyEndpoint { +public class JWKSetPublishingEndpoint { + + public static final String URL = "jwk"; @Autowired - private JwtSigningAndValidationService jwtService; + private JWTSigningAndValidationService jwtService; - @RequestMapping(value = "/jwk", produces = "application/json") + @RequestMapping(value = "/" + URL, produces = MediaType.APPLICATION_JSON_VALUE) public String getJwk(Model m) { // map from key id to key @@ -42,20 +47,20 @@ public String getJwk(Model m) { m.addAttribute("keys", keys); - return "jwkKeyList"; + return JWKSetView.VIEWNAME; } /** * @return the jwtService */ - public JwtSigningAndValidationService getJwtService() { + public JWTSigningAndValidationService getJwtService() { return jwtService; } /** * @param jwtService the jwtService to set */ - public void setJwtService(JwtSigningAndValidationService jwtService) { + public void setJwtService(JWTSigningAndValidationService jwtService) { this.jwtService = jwtService; } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java index 6e55d73eda..5be40caa34 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java @@ -1,19 +1,18 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * * 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 org.mitre.openid.connect.web; import java.io.UnsupportedEncodingException; @@ -22,7 +21,6 @@ import java.util.HashSet; import java.util.Set; -import org.mitre.jwt.signer.service.JwtSigningAndValidationService; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; @@ -34,12 +32,15 @@ import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor; import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.mitre.openid.connect.exception.ValidationException; -import org.mitre.openid.connect.service.BlacklistedSiteService; import org.mitre.openid.connect.service.OIDCTokenService; +import org.mitre.openid.connect.view.ClientInformationResponseView; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonErrorView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; @@ -52,35 +53,36 @@ import org.springframework.web.util.UriUtils; import com.google.common.base.Strings; -import com.google.common.collect.Sets; import com.google.gson.JsonSyntaxException; @Controller -@RequestMapping(value = "resource") +@RequestMapping(value = ProtectedResourceRegistrationEndpoint.URL) public class ProtectedResourceRegistrationEndpoint { + /** + * + */ + public static final String URL = "resource"; + @Autowired private ClientDetailsEntityService clientService; @Autowired private OAuth2TokenEntityService tokenService; - @Autowired - private JwtSigningAndValidationService jwtService; - @Autowired private SystemScopeService scopeService; - @Autowired - private BlacklistedSiteService blacklistService; - @Autowired private ConfigurationPropertiesBean config; @Autowired private OIDCTokenService connectTokenService; - private static Logger logger = LoggerFactory.getLogger(ProtectedResourceRegistrationEndpoint.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(ProtectedResourceRegistrationEndpoint.class); /** * Create a new Client, issue a client ID, and create a registration access token. @@ -89,7 +91,7 @@ public class ProtectedResourceRegistrationEndpoint { * @param p * @return */ - @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String registerNewProtectedResource(@RequestBody String jsonString, Model m) { ClientDetailsEntity newClient = null; @@ -99,8 +101,8 @@ public String registerNewProtectedResource(@RequestBody String jsonString, Model // bad parse // didn't parse, this is a bad request logger.error("registerNewProtectedResource failed; submitted JSON is malformed"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - return "httpCodeView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + return HttpCodeView.VIEWNAME; } if (newClient != null) { @@ -120,10 +122,10 @@ public String registerNewProtectedResource(@RequestBody String jsonString, Model newClient = validateAuth(newClient); } catch (ValidationException ve) { // validation failed, return an error - m.addAttribute("error", ve.getError()); - m.addAttribute("errorMessage", ve.getErrorDescription()); - m.addAttribute("code", ve.getStatus()); - return "jsonErrorView"; + m.addAttribute(JsonErrorView.ERROR, ve.getError()); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); + m.addAttribute(HttpCodeView.CODE, ve.getStatus()); + return JsonErrorView.VIEWNAME; } @@ -144,7 +146,7 @@ public String registerNewProtectedResource(@RequestBody String jsonString, Model newClient.setIdTokenEncryptedResponseEnc(null); newClient.setIdTokenSignedResponseAlg(null); newClient.setInitiateLoginUri(null); - newClient.setPostLogoutRedirectUri(null); + newClient.setPostLogoutRedirectUris(null); newClient.setRequestObjectSigningAlg(null); newClient.setRequireAuthTime(null); newClient.setReuseRefreshToken(false); @@ -172,41 +174,34 @@ public String registerNewProtectedResource(@RequestBody String jsonString, Model RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); m.addAttribute("client", registered); - m.addAttribute("code", HttpStatus.CREATED); // http 201 + m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201 - return "clientInformationResponseView"; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR); - return "httpCodeView"; + return ClientInformationResponseView.VIEWNAME; } catch (IllegalArgumentException e) { logger.error("Couldn't save client", e); - - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Unable to save client due to invalid or inconsistent metadata."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - - return "jsonErrorView"; + + m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + + return JsonErrorView.VIEWNAME; } } else { // didn't parse, this is a bad request logger.error("registerNewClient failed; submitted JSON is malformed"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException { - // set of scopes that are OK for clients to dynamically register for - Set dynScopes = scopeService.getDynReg(); - // scopes that the client is asking for Set requestedScopes = scopeService.fromStrings(newClient.getScope()); // the scopes that the client can have must be a subset of the dynamically allowed scopes - Set allowedScopes = Sets.intersection(dynScopes, requestedScopes); + Set allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes); // if the client didn't ask for any, give them the defaults if (allowedScopes == null || allowedScopes.isEmpty()) { @@ -214,7 +209,7 @@ private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws } newClient.setScope(scopeService.toStrings(allowedScopes)); - + return newClient; } @@ -226,38 +221,31 @@ private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws * @return */ @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String readResourceConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { ClientDetailsEntity client = clientService.loadClientByClientId(clientId); if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { + + // possibly update the token + OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client); + RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); + // send it all out to the view + m.addAttribute("client", registered); + m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 - try { - // possibly update the token - OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client); - - RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); - - // send it all out to the view - m.addAttribute("client", registered); - m.addAttribute("code", HttpStatus.OK); // http 200 - - return "clientInformationResponseView"; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR); - return "httpCodeView"; - } + return ClientInformationResponseView.VIEWNAME; + } else { // client mismatch logger.error("readResourceConfiguration failed, client ID mismatch: " + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } @@ -270,7 +258,7 @@ public String readResourceConfiguration(@PathVariable("id") String clientId, Mod * @return */ @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = "application/json", consumes = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public String updateProtectedResource(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) { @@ -281,8 +269,8 @@ public String updateProtectedResource(@PathVariable("id") String clientId, @Requ // bad parse // didn't parse, this is a bad request logger.error("updateProtectedResource failed; submitted JSON is malformed"); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - return "httpCodeView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + return HttpCodeView.VIEWNAME; } ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); @@ -314,7 +302,7 @@ public String updateProtectedResource(@PathVariable("id") String clientId, @Requ newClient.setIdTokenEncryptedResponseEnc(null); newClient.setIdTokenSignedResponseAlg(null); newClient.setInitiateLoginUri(null); - newClient.setPostLogoutRedirectUri(null); + newClient.setPostLogoutRedirectUris(null); newClient.setRequestObjectSigningAlg(null); newClient.setRequireAuthTime(null); newClient.setReuseRefreshToken(false); @@ -336,10 +324,10 @@ public String updateProtectedResource(@PathVariable("id") String clientId, @Requ newClient = validateAuth(newClient); } catch (ValidationException ve) { // validation failed, return an error - m.addAttribute("error", ve.getError()); - m.addAttribute("errorMessage", ve.getErrorDescription()); - m.addAttribute("code", ve.getStatus()); - return "jsonErrorView"; + m.addAttribute(JsonErrorView.ERROR, ve.getError()); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); + m.addAttribute(HttpCodeView.CODE, ve.getStatus()); + return JsonErrorView.VIEWNAME; } @@ -354,30 +342,26 @@ public String updateProtectedResource(@PathVariable("id") String clientId, @Requ // send it all out to the view m.addAttribute("client", registered); - m.addAttribute("code", HttpStatus.OK); // http 200 + m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 - return "clientInformationResponseView"; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR); - return "httpCodeView"; + return ClientInformationResponseView.VIEWNAME; } catch (IllegalArgumentException e) { logger.error("Couldn't save client", e); - - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Unable to save client due to invalid or inconsistent metadata."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 - - return "jsonErrorView"; + + m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + + return JsonErrorView.VIEWNAME; } } else { // client mismatch logger.error("updateProtectedResource" + " failed, client ID mismatch: " + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } @@ -389,7 +373,7 @@ public String updateProtectedResource(@PathVariable("id") String clientId, @Requ * @return */ @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = "application/json") + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) public String deleteResource(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { ClientDetailsEntity client = clientService.loadClientByClientId(clientId); @@ -398,16 +382,16 @@ public String deleteResource(@PathVariable("id") String clientId, Model m, OAuth clientService.deleteClient(client); - m.addAttribute("code", HttpStatus.NO_CONTENT); // http 204 + m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } else { // client mismatch logger.error("readClientConfiguration failed, client ID mismatch: " + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } } @@ -425,10 +409,10 @@ private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws V newClient = clientService.generateClientSecret(newClient); } } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { - if (Strings.isNullOrEmpty(newClient.getJwksUri())) { + if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); } - + newClient.setClientSecret(null); } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) { newClient.setClientSecret(null); @@ -437,14 +421,14 @@ private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws V } return newClient; } - + private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication auth, ClientDetailsEntity client) { - + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); - + if (config.getRegTokenLifeTime() != null) { - + try { // Re-issue the token if it has been issued before [currentTime - validity] Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000); @@ -467,4 +451,5 @@ private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication return token; } } + } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ManagerController.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/RootController.java similarity index 88% rename from openid-connect-server/src/main/java/org/mitre/openid/connect/web/ManagerController.java rename to openid-connect-server/src/main/java/org/mitre/openid/connect/web/RootController.java index 0192d3270e..6f08be5e06 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ManagerController.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/RootController.java @@ -1,19 +1,20 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.util.Map; @@ -30,29 +31,25 @@ */ @Controller -public class ManagerController { +public class RootController { + + public static final String API_URL = "api"; @Autowired private StatsService statsService; @RequestMapping({"", "home", "index"}) public String showHomePage(ModelMap m) { - - //Map summary = statsService.getSummaryStats(); - - //m.put("statsSummary", summary); return "home"; } @RequestMapping({"about", "about/"}) public String showAboutPage(ModelMap m) { - return "about"; } @RequestMapping({"stats", "stats/"}) public String showStatsPage(ModelMap m) { - Map summary = statsService.getSummaryStats(); m.put("statsSummary", summary); @@ -61,7 +58,6 @@ public String showStatsPage(ModelMap m) { @RequestMapping({"contact", "contact/"}) public String showContactPage(ModelMap m) { - return "contact"; } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ServerConfigInterceptor.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ServerConfigInterceptor.java index 0e1d2ab508..f16693613f 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ServerConfigInterceptor.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ServerConfigInterceptor.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; @@ -23,14 +24,15 @@ import javax.servlet.http.HttpServletResponse; import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.config.UIConfiguration; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; /** - * - * Injects the server configuration bean into the Model context, if it exists. Allows JSPs and the like to call "config.logoUrl" among others. - * + * + * Injects the server configuration bean into the request context. + * This allows JSPs and the like to call "config.logoUrl" among others. + * * @author jricher * */ @@ -39,11 +41,14 @@ public class ServerConfigInterceptor extends HandlerInterceptorAdapter { @Autowired private ConfigurationPropertiesBean config; + @Autowired + private UIConfiguration ui; + @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { - if (modelAndView != null) { // skip checking at all if we have no model and view to hand the config to - modelAndView.addObject("config", config); - } + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + request.setAttribute("config", config); + request.setAttribute("ui", ui); + return true; } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/StatsAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/StatsAPI.java index 9a71708076..d090f77216 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/StatsAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/StatsAPI.java @@ -1,25 +1,31 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.util.Map; +import org.mitre.openid.connect.model.ClientStat; import org.mitre.openid.connect.service.StatsService; +import org.mitre.openid.connect.view.JsonEntityView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -27,41 +33,46 @@ import org.springframework.web.bind.annotation.RequestMapping; @Controller -@RequestMapping("/api/stats") +@RequestMapping("/" + StatsAPI.URL) public class StatsAPI { + public static final String URL = RootController.API_URL + "/stats"; + + // Logger for this class + private static final Logger logger = LoggerFactory.getLogger(StatsAPI.class); + @Autowired private StatsService statsService; - @RequestMapping(value = "summary", produces = "application/json") + @RequestMapping(value = "summary", produces = MediaType.APPLICATION_JSON_VALUE) public String statsSummary(ModelMap m) { Map e = statsService.getSummaryStats(); - m.put("entity", e); - - return "jsonEntityView"; - - } - - @PreAuthorize("hasRole('ROLE_USER')") - @RequestMapping(value = "byclientid", produces = "application/json") - public String statsByClient(ModelMap m) { - Map e = statsService.getByClientId(); + m.put(JsonEntityView.ENTITY, e); - m.put("entity", e); + return JsonEntityView.VIEWNAME; - return "jsonEntityView"; } + // @PreAuthorize("hasRole('ROLE_USER')") + // @RequestMapping(value = "byclientid", produces = MediaType.APPLICATION_JSON_VALUE) + // public String statsByClient(ModelMap m) { + // Map e = statsService.getByClientId(); + // + // m.put(JsonEntityView.ENTITY, e); + // + // return JsonEntityView.VIEWNAME; + // } + // @PreAuthorize("hasRole('ROLE_USER')") - @RequestMapping(value = "byclientid/{id}", produces = "application/json") - public String statsByClientId(@PathVariable("id") Long id, ModelMap m) { - Integer e = statsService.getCountForClientId(id); + @RequestMapping(value = "byclientid/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public String statsByClientId(@PathVariable("id") String clientId, ModelMap m) { + ClientStat e = statsService.getCountForClientId(clientId); - m.put("entity", e); + m.put(JsonEntityView.ENTITY, e); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/UserInfoEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/UserInfoEndpoint.java index bf7167d2f3..16950a0926 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/UserInfoEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/UserInfoEndpoint.java @@ -1,30 +1,36 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; import java.util.List; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.SystemScopeService; import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.service.UserInfoService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.UserInfoJWTView; +import org.mitre.openid.connect.view.UserInfoView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -40,36 +46,40 @@ /** * OpenID Connect UserInfo endpoint, as specified in Standard sec 5 and Messages sec 2.4. - * + * * @author AANGANES * */ @Controller +@RequestMapping("/" + UserInfoEndpoint.URL) public class UserInfoEndpoint { + public static final String URL = "userinfo"; + @Autowired private UserInfoService userInfoService; @Autowired private ClientDetailsEntityService clientService; - private static Logger logger = LoggerFactory.getLogger(UserInfoEndpoint.class); - - private static final MediaType JOSE_MEDIA_TYPE = new MediaType("application", "jwt"); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(UserInfoEndpoint.class); /** * Get information about the user as specified in the accessToken included in this request */ - @PreAuthorize("hasRole('ROLE_USER') and #oauth2.hasScope('openid')") - @RequestMapping(value="/userinfo", method= {RequestMethod.GET, RequestMethod.POST}, produces = {"application/json", "application/jwt"}) + @PreAuthorize("hasRole('ROLE_USER') and #oauth2.hasScope('" + SystemScopeService.OPENID_SCOPE + "')") + @RequestMapping(method= {RequestMethod.GET, RequestMethod.POST}, produces = {MediaType.APPLICATION_JSON_VALUE, UserInfoJWTView.JOSE_MEDIA_TYPE_VALUE}) public String getInfo(@RequestParam(value="claims", required=false) String claimsRequestJsonString, - @RequestHeader(value="Accept", required=false) String acceptHeader, + @RequestHeader(value=HttpHeaders.ACCEPT, required=false) String acceptHeader, OAuth2Authentication auth, Model model) { if (auth == null) { logger.error("getInfo failed; no principal. Requester is not authorized."); - model.addAttribute("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; + model.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; } String username = auth.getName(); @@ -77,55 +87,55 @@ public String getInfo(@RequestParam(value="claims", required=false) String claim if (userInfo == null) { logger.error("getInfo failed; user not found: " + username); - model.addAttribute("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; + model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; } - model.addAttribute("scope", auth.getOAuth2Request().getScope()); + model.addAttribute(UserInfoView.SCOPE, auth.getOAuth2Request().getScope()); - model.addAttribute("authorizedClaims", auth.getOAuth2Request().getExtensions().get("claims")); + model.addAttribute(UserInfoView.AUTHORIZED_CLAIMS, auth.getOAuth2Request().getExtensions().get("claims")); if (!Strings.isNullOrEmpty(claimsRequestJsonString)) { - model.addAttribute("requestedClaims", claimsRequestJsonString); + model.addAttribute(UserInfoView.REQUESTED_CLAIMS, claimsRequestJsonString); } - model.addAttribute("userInfo", userInfo); + model.addAttribute(UserInfoView.USER_INFO, userInfo); // content negotiation // start off by seeing if the client has registered for a signed/encrypted JWT from here ClientDetailsEntity client = clientService.loadClientByClientId(auth.getOAuth2Request().getClientId()); - model.addAttribute("client", client); - + model.addAttribute(UserInfoJWTView.CLIENT, client); + List mediaTypes = MediaType.parseMediaTypes(acceptHeader); MediaType.sortBySpecificityAndQuality(mediaTypes); - - if (client.getUserInfoSignedResponseAlg() != null + + if (client.getUserInfoSignedResponseAlg() != null || client.getUserInfoEncryptedResponseAlg() != null || client.getUserInfoEncryptedResponseEnc() != null) { // client has a preference, see if they ask for plain JSON specifically on this request for (MediaType m : mediaTypes) { - if (!m.isWildcardType() && m.isCompatibleWith(JOSE_MEDIA_TYPE)) { - return "userInfoJwtView"; + if (!m.isWildcardType() && m.isCompatibleWith(UserInfoJWTView.JOSE_MEDIA_TYPE)) { + return UserInfoJWTView.VIEWNAME; } else if (!m.isWildcardType() && m.isCompatibleWith(MediaType.APPLICATION_JSON)) { - return "userInfoView"; + return UserInfoView.VIEWNAME; } } - + // otherwise return JWT - return "userInfoJwtView"; + return UserInfoJWTView.VIEWNAME; } else { // client has no preference, see if they asked for JWT specifically on this request for (MediaType m : mediaTypes) { if (!m.isWildcardType() && m.isCompatibleWith(MediaType.APPLICATION_JSON)) { - return "userInfoView"; - } else if (!m.isWildcardType() && m.isCompatibleWith(JOSE_MEDIA_TYPE)) { - return "userInfoJwtView"; + return UserInfoView.VIEWNAME; + } else if (!m.isWildcardType() && m.isCompatibleWith(UserInfoJWTView.JOSE_MEDIA_TYPE)) { + return UserInfoJWTView.VIEWNAME; } } // otherwise return JSON - return "userInfoView"; + return UserInfoView.VIEWNAME; } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/WhitelistAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/WhitelistAPI.java index 7d408fc08e..8450c88ce2 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/WhitelistAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/WhitelistAPI.java @@ -1,21 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.web; @@ -24,10 +25,14 @@ import org.mitre.openid.connect.model.WhitelistedSite; import org.mitre.openid.connect.service.WhitelistedSiteService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -46,14 +51,19 @@ * */ @Controller -@RequestMapping("/api/whitelist") +@RequestMapping("/" + WhitelistAPI.URL) @PreAuthorize("hasRole('ROLE_USER')") public class WhitelistAPI { + public static final String URL = RootController.API_URL + "/whitelist"; + @Autowired private WhitelistedSiteService whitelistService; - private static Logger logger = LoggerFactory.getLogger(WhitelistAPI.class); + /** + * Logger for this class + */ + private static final Logger logger = LoggerFactory.getLogger(WhitelistAPI.class); private Gson gson = new Gson(); private JsonParser parser = new JsonParser(); @@ -63,14 +73,14 @@ public class WhitelistAPI { * @param m * @return */ - @RequestMapping(method = RequestMethod.GET, produces = "application/json") + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getAllWhitelistedSites(ModelMap m) { Collection all = whitelistService.getAll(); - m.put("entity", all); + m.put(JsonEntityView.ENTITY, all); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } /** @@ -81,7 +91,7 @@ public String getAllWhitelistedSites(ModelMap m) { * @return */ @PreAuthorize("hasRole('ROLE_ADMIN')") - @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String addNewWhitelistedSite(@RequestBody String jsonString, ModelMap m, Principal p) { JsonObject json; @@ -93,14 +103,14 @@ public String addNewWhitelistedSite(@RequestBody String jsonString, ModelMap m, } catch (JsonParseException e) { logger.error("addNewWhitelistedSite failed due to JsonParseException", e); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Could not save new whitelisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new whitelisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } catch (IllegalStateException e) { logger.error("addNewWhitelistedSite failed due to IllegalStateException", e); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - m.addAttribute("errorMessage", "Could not save new whitelisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); - return "jsonErrorView"; + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new whitelisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } // save the id of the person who created this @@ -108,9 +118,9 @@ public String addNewWhitelistedSite(@RequestBody String jsonString, ModelMap m, WhitelistedSite newWhitelist = whitelistService.saveNew(whitelist); - m.put("entity", newWhitelist); + m.put(JsonEntityView.ENTITY, newWhitelist); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } @@ -118,7 +128,7 @@ public String addNewWhitelistedSite(@RequestBody String jsonString, ModelMap m, * Update an existing whitelisted site */ @PreAuthorize("hasRole('ROLE_ADMIN')") - @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json") + @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String updateWhitelistedSite(@PathVariable("id") Long id, @RequestBody String jsonString, ModelMap m, Principal p) { JsonObject json; @@ -130,36 +140,36 @@ public String updateWhitelistedSite(@PathVariable("id") Long id, @RequestBody St } catch (JsonParseException e) { logger.error("updateWhitelistedSite failed due to JsonParseException", e); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not update whitelisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update whitelisted site. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } catch (IllegalStateException e) { logger.error("updateWhitelistedSite failed due to IllegalStateException", e); - m.put("code", HttpStatus.BAD_REQUEST); - m.put("errorMessage", "Could not update whitelisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update whitelisted site. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); + return JsonErrorView.VIEWNAME; } WhitelistedSite oldWhitelist = whitelistService.getById(id); if (oldWhitelist == null) { logger.error("updateWhitelistedSite failed; whitelist with id " + id + " could not be found."); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "Could not update whitelisted site. The requested whitelisted site with id " + id + "could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not update whitelisted site. The requested whitelisted site with id " + id + "could not be found."); + return JsonErrorView.VIEWNAME; } else { WhitelistedSite newWhitelist = whitelistService.update(oldWhitelist, whitelist); - m.put("entity", newWhitelist); + m.put(JsonEntityView.ENTITY, newWhitelist); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } } /** * Delete a whitelisted site - * + * */ @PreAuthorize("hasRole('ROLE_ADMIN')") @RequestMapping(value="/{id}", method = RequestMethod.DELETE) @@ -168,34 +178,35 @@ public String deleteWhitelistedSite(@PathVariable("id") Long id, ModelMap m) { if (whitelist == null) { logger.error("deleteWhitelistedSite failed; whitelist with id " + id + " could not be found."); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "Could not delete whitelisted site. The requested whitelisted site with id " + id + "could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "Could not delete whitelisted site. The requested whitelisted site with id " + id + "could not be found."); + return JsonErrorView.VIEWNAME; } else { - m.put("code", HttpStatus.OK); + m.put(HttpCodeView.CODE, HttpStatus.OK); whitelistService.remove(whitelist); } - return "httpCodeView"; + return HttpCodeView.VIEWNAME; } /** * Get a single whitelisted site */ - @RequestMapping(value="/{id}", method = RequestMethod.GET, produces = "application/json") + @RequestMapping(value="/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public String getWhitelistedSite(@PathVariable("id") Long id, ModelMap m) { WhitelistedSite whitelist = whitelistService.getById(id); if (whitelist == null) { logger.error("getWhitelistedSite failed; whitelist with id " + id + " could not be found."); - m.put("code", HttpStatus.NOT_FOUND); - m.put("errorMessage", "The requested whitelisted site with id " + id + "could not be found."); - return "jsonErrorView"; + m.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.put(JsonErrorView.ERROR_MESSAGE, "The requested whitelisted site with id " + id + "could not be found."); + return JsonErrorView.VIEWNAME; } else { - m.put("entity", whitelist); + m.put(JsonEntityView.ENTITY, whitelist); - return "jsonEntityView"; + return JsonEntityView.VIEWNAME; } } + } diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/repository/impl/TestDatabaseConfiguration.java b/openid-connect-server/src/test/java/org/mitre/oauth2/repository/impl/TestDatabaseConfiguration.java new file mode 100644 index 0000000000..51ab0f43c5 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/repository/impl/TestDatabaseConfiguration.java @@ -0,0 +1,96 @@ +package org.mitre.oauth2.repository.impl; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +public class TestDatabaseConfiguration { + + @Autowired + private JpaVendorAdapter jpaAdapter; + + @Autowired + private DataSource dataSource; + + @Autowired + private EntityManagerFactory entityManagerFactory; + + @Bean + public JpaOAuth2TokenRepository repository() { + return new JpaOAuth2TokenRepository(); + } + + @Bean(name = "defaultPersistenceUnit") + public FactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setPackagesToScan("org.mitre", "org.mitre"); + factory.setPersistenceProviderClass(org.eclipse.persistence.jpa.PersistenceProvider.class); + factory.setPersistenceUnitName("test" + System.currentTimeMillis()); + factory.setDataSource(dataSource); + factory.setJpaVendorAdapter(jpaAdapter); + Map jpaProperties = new HashMap(); + jpaProperties.put("eclipselink.weaving", "false"); + jpaProperties.put("eclipselink.logging.level", "INFO"); + jpaProperties.put("eclipselink.logging.level.sql", "INFO"); + jpaProperties.put("eclipselink.cache.shared.default", "false"); + factory.setJpaPropertyMap(jpaProperties); + + return factory; + } + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder(new DefaultResourceLoader() { + @Override + public Resource getResource(String location) { + String sql; + try { + sql = new String(Files.readAllBytes(Paths.get("..", "openid-connect-server-webapp", "src", "main", + "resources", "db", "hsql", location)), UTF_8); + } catch (IOException e) { + throw new RuntimeException("Failed to read sql-script " + location, e); + } + + return new ByteArrayResource(sql.getBytes(UTF_8)); + } + }).generateUniqueName(true).setScriptEncoding(UTF_8.name()).setType(EmbeddedDatabaseType.HSQL) + .addScripts("hsql_database_tables.sql").build(); + } + + @Bean + public JpaVendorAdapter jpaAdapter() { + EclipseLinkJpaVendorAdapter adapter = new EclipseLinkJpaVendorAdapter(); + adapter.setDatabase(Database.HSQL); + adapter.setShowSql(true); + return adapter; + } + + @Bean + public PlatformTransactionManager transactionManager() { + JpaTransactionManager platformTransactionManager = new JpaTransactionManager(); + platformTransactionManager.setEntityManagerFactory(entityManagerFactory); + return platformTransactionManager; + } +} diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/repository/impl/TestJpaOAuth2TokenRepository.java b/openid-connect-server/src/test/java/org/mitre/oauth2/repository/impl/TestJpaOAuth2TokenRepository.java new file mode 100644 index 0000000000..4ad4b355cc --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/repository/impl/TestJpaOAuth2TokenRepository.java @@ -0,0 +1,107 @@ +package org.mitre.oauth2.repository.impl; + +import static org.junit.Assert.assertEquals; + +import java.util.Set; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SavedUserAuthentication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = { TestDatabaseConfiguration.class }) +@Transactional +public class TestJpaOAuth2TokenRepository { + + @Autowired + private JpaOAuth2TokenRepository repository; + + @PersistenceContext + private EntityManager entityManager; + + @Before + public void setUp(){ + createAccessToken("user1"); + createAccessToken("user1"); + createAccessToken("user2"); + createAccessToken("user2"); + + createRefreshToken("user1"); + createRefreshToken("user1"); + createRefreshToken("user2"); + createRefreshToken("user2"); + createRefreshToken("user2"); + } + + @Test + public void testGetAccessTokensByUserName() { + Set tokens = repository.getAccessTokensByUserName("user1"); + assertEquals(2, tokens.size()); + assertEquals("user1", tokens.iterator().next().getAuthenticationHolder().getUserAuth().getName()); + } + + @Test + public void testGetRefreshTokensByUserName() { + Set tokens = repository.getRefreshTokensByUserName("user2"); + assertEquals(3, tokens.size()); + assertEquals("user2", tokens.iterator().next().getAuthenticationHolder().getUserAuth().getName()); + } + + @Test + public void testGetAllAccessTokens(){ + Set tokens = repository.getAllAccessTokens(); + assertEquals(4, tokens.size()); + } + + @Test + public void testGetAllRefreshTokens(){ + Set tokens = repository.getAllRefreshTokens(); + assertEquals(5, tokens.size()); + } + + private OAuth2AccessTokenEntity createAccessToken(String name) { + SavedUserAuthentication userAuth = new SavedUserAuthentication(); + userAuth.setName(name); + userAuth = entityManager.merge(userAuth); + + AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); + authHolder.setUserAuth(userAuth); + authHolder = entityManager.merge(authHolder); + + OAuth2AccessTokenEntity accessToken = new OAuth2AccessTokenEntity(); + accessToken.setAuthenticationHolder(authHolder); + + accessToken = entityManager.merge(accessToken); + + return accessToken; + } + + private OAuth2RefreshTokenEntity createRefreshToken(String name) { + SavedUserAuthentication userAuth = new SavedUserAuthentication(); + userAuth.setName(name); + userAuth = entityManager.merge(userAuth); + + AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); + authHolder.setUserAuth(userAuth); + authHolder = entityManager.merge(authHolder); + + OAuth2RefreshTokenEntity refreshToken = new OAuth2RefreshTokenEntity(); + refreshToken.setAuthenticationHolder(authHolder); + + refreshToken = entityManager.merge(refreshToken); + + return refreshToken; + } + +} diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestBlacklistAwareRedirectResolver.java b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestBlacklistAwareRedirectResolver.java new file mode 100644 index 0000000000..3698ec9e05 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestBlacklistAwareRedirectResolver.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service.impl; + +import static org.mockito.Matchers.anyString; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.service.BlacklistedSiteService; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; +import org.springframework.security.oauth2.provider.ClientDetails; + +import com.google.common.collect.ImmutableSet; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.mockito.Mockito.when; + +import static org.junit.Assert.assertThat; + +/** + * @author jricher + * + */ +@RunWith(MockitoJUnitRunner.class) +public class TestBlacklistAwareRedirectResolver { + + @Mock + private BlacklistedSiteService blacklistService; + + @Mock + private ClientDetails client; + + @Mock + private ConfigurationPropertiesBean config; + + @InjectMocks + private BlacklistAwareRedirectResolver resolver; + + private String blacklistedUri = "https://evil.example.com/"; + + private String goodUri = "https://good.example.com/"; + + private String pathUri = "https://good.example.com/with/path"; + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + + when(blacklistService.isBlacklisted(anyString())).thenReturn(false); + when(blacklistService.isBlacklisted(blacklistedUri)).thenReturn(true); + + when(client.getAuthorizedGrantTypes()).thenReturn(ImmutableSet.of("authorization_code")); + when(client.getRegisteredRedirectUri()).thenReturn(ImmutableSet.of(goodUri, blacklistedUri)); + + when(config.isHeartMode()).thenReturn(false); + } + + @Test + public void testResolveRedirect_safe() { + + // default uses prefix matching, the first one should work fine + + String res1 = resolver.resolveRedirect(goodUri, client); + + assertThat(res1, is(equalTo(goodUri))); + + // set the resolver to non-strict and test the path-based redirect resolution + + resolver.setStrictMatch(false); + + String res2 = resolver.resolveRedirect(pathUri, client); + + assertThat(res2, is(equalTo(pathUri))); + + + } + + @Test(expected = InvalidRequestException.class) + public void testResolveRedirect_blacklisted() { + + // this should fail with an error + resolver.resolveRedirect(blacklistedUri, client); + + } + + @Test + public void testRedirectMatches_default() { + + // this is not an exact match + boolean res1 = resolver.redirectMatches(pathUri, goodUri); + + assertThat(res1, is(false)); + + // this is an exact match + boolean res2 = resolver.redirectMatches(goodUri, goodUri); + + assertThat(res2, is(true)); + + } + + @Test + public void testRedirectMatches_nonstrict() { + + // set the resolver to non-strict match mode + resolver.setStrictMatch(false); + + // this is not an exact match (but that's OK) + boolean res1 = resolver.redirectMatches(pathUri, goodUri); + + assertThat(res1, is(true)); + + // this is an exact match + boolean res2 = resolver.redirectMatches(goodUri, goodUri); + + assertThat(res2, is(true)); + + } + + @Test + public void testHeartMode() { + when(config.isHeartMode()).thenReturn(true); + + // this is not an exact match + boolean res1 = resolver.redirectMatches(pathUri, goodUri); + + assertThat(res1, is(false)); + + // this is an exact match + boolean res2 = resolver.redirectMatches(goodUri, goodUri); + + assertThat(res2, is(true)); + } + +} diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionAuthorizer.java b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionAuthorizer.java deleted file mode 100755 index 0dde0e0ce4..0000000000 --- a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionAuthorizer.java +++ /dev/null @@ -1,114 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.oauth2.service.impl; - -import java.util.Set; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mitre.oauth2.service.SystemScopeService; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.security.oauth2.provider.ClientDetails; - -import static com.google.common.collect.Sets.newHashSet; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -@RunWith(MockitoJUnitRunner.class) -public class TestDefaultIntrospectionAuthorizer { - - @InjectMocks - private DefaultIntrospectionAuthorizer introspectionPermitter; - - @Mock - private SystemScopeService scopeService; - - @Test - public void shouldPermitIntrospectionToSameClientTheTokenWasIssuedTo() { - - // given - String sameClient = "same"; - - // when - boolean permitted = introspectionPermitter.isIntrospectionPermitted( - clientWithId(sameClient), clientWithId(sameClient), - scope("scope")); - - // then - assertThat(permitted, is(true)); - } - - @Test - public void shouldPermitIntrospectionToDifferentClientIfScopesMatch() { - - // given - String authClient = "auth"; - String tokenClient = "token"; - Set authScope = scope("scope1", "scope2", "scope3"); - Set tokenScope = scope("scope1", "scope2"); - given(scopeService.scopesMatch(authScope, tokenScope)).willReturn(true); - - // when - boolean permitted = introspectionPermitter.isIntrospectionPermitted( - clientWithIdAndScope(authClient, authScope), - clientWithId(tokenClient), tokenScope); - - // then - assertThat(permitted, is(true)); - } - - @Test - public void shouldNotPermitIntrospectionToDifferentClientIfScopesDontMatch() { - - // given - String authClient = "auth"; - String tokenClient = "token"; - Set authScope = scope("scope1", "scope2"); - Set tokenScope = scope("scope1", "scope2", "scope3"); - given(scopeService.scopesMatch(authScope, tokenScope)) - .willReturn(false); - - // when - boolean permitted = introspectionPermitter.isIntrospectionPermitted( - clientWithIdAndScope(authClient, authScope), - clientWithId(tokenClient), tokenScope); - - // then - assertThat(permitted, is(false)); - } - - private ClientDetails clientWithId(String clientId) { - ClientDetails client = mock(ClientDetails.class); - given(client.getClientId()).willReturn(clientId); - return client; - } - - private ClientDetails clientWithIdAndScope(String clientId, - Set scope) { - ClientDetails client = clientWithId(clientId); - given(client.getScope()).willReturn(scope); - return client; - } - - private Set scope(String... scopeItems) { - return newHashSet(scopeItems); - } -} diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionResultAssembler.java b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionResultAssembler.java new file mode 100644 index 0000000000..423cab6da0 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionResultAssembler.java @@ -0,0 +1,362 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.oauth2.service.impl; + +import static com.google.common.collect.Sets.newHashSet; +import static org.mockito.BDDMockito.given; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.Set; + +import javax.swing.text.DateFormatter; + +import org.junit.Test; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.service.IntrospectionResultAssembler; +import org.mitre.openid.connect.model.UserInfo; +import org.mitre.uma.model.Permission; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; + +import static org.junit.Assert.assertThat; + +public class TestDefaultIntrospectionResultAssembler { + + private IntrospectionResultAssembler assembler = new DefaultIntrospectionResultAssembler(); + + private static DateFormatter dateFormat = new DateFormatter(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")); + + @Test + public void shouldAssembleExpectedResultForAccessToken() throws ParseException { + + // given + OAuth2AccessTokenEntity accessToken = accessToken(new Date(123 * 1000L), scopes("foo", "bar"), null, "Bearer", + oauth2AuthenticationWithUser(oauth2Request("clientId"), "name")); + + UserInfo userInfo = userInfo("sub"); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(accessToken, userInfo, authScopes); + + + // then + Map expected = new ImmutableMap.Builder() + .put("sub", "sub") + .put("exp", 123L) + .put("expires_at", dateFormat.valueToString(new Date(123 * 1000L))) + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("user_id", "name") + .put("client_id", "clientId") + .put("token_type", "Bearer") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForAccessToken_withPermissions() throws ParseException { + + // given + OAuth2AccessTokenEntity accessToken = accessToken(new Date(123 * 1000L), scopes("foo", "bar"), + permissions(permission(1L, "foo", "bar")), + "Bearer", oauth2AuthenticationWithUser(oauth2Request("clientId"), "name")); + + UserInfo userInfo = userInfo("sub"); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(accessToken, userInfo, authScopes); + + + // then + Map expected = new ImmutableMap.Builder() + .put("sub", "sub") + .put("exp", 123L) + .put("expires_at", dateFormat.valueToString(new Date(123 * 1000L))) + .put("permissions", new ImmutableSet.Builder<>() + .add(new ImmutableMap.Builder() + .put("resource_set_id", "1") // note that the resource ID comes out as a string + .put("scopes", new ImmutableSet.Builder<>() + .add("bar") + .add("foo") + .build()) + .build()) + .build()) + // note that scopes are not included if permissions are included + .put("active", Boolean.TRUE) + .put("user_id", "name") + .put("client_id", "clientId") + .put("token_type", "Bearer") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForAccessTokenWithoutUserInfo() throws ParseException { + + // given + OAuth2AccessTokenEntity accessToken = accessToken(new Date(123 * 1000L), scopes("foo", "bar"), null, "Bearer", + oauth2AuthenticationWithUser(oauth2Request("clientId"), "name")); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(accessToken, null, authScopes); + + + // then + Map expected = new ImmutableMap.Builder() + .put("sub", "name") + .put("exp", 123L) + .put("expires_at", dateFormat.valueToString(new Date(123 * 1000L))) + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("user_id", "name") + .put("client_id", "clientId") + .put("token_type", "Bearer") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForAccessTokenWithoutExpiry() { + + // given + OAuth2AccessTokenEntity accessToken = accessToken(null, scopes("foo", "bar"), null, "Bearer", + oauth2AuthenticationWithUser(oauth2Request("clientId"), "name")); + + UserInfo userInfo = userInfo("sub"); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(accessToken, userInfo, authScopes); + + + // then + Map expected = new ImmutableMap.Builder() + .put("sub", "sub") + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("user_id", "name") + .put("client_id", "clientId") + .put("token_type", "Bearer") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForAccessTokenWithoutUserAuthentication() throws ParseException { + // given + OAuth2AccessTokenEntity accessToken = accessToken(new Date(123 * 1000L), scopes("foo", "bar"), null, "Bearer", + oauth2Authentication(oauth2Request("clientId"), null)); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(accessToken, null, authScopes); + + + // then `user_id` should not be present + Map expected = new ImmutableMap.Builder() + .put("sub", "clientId") + .put("exp", 123L) + .put("expires_at", dateFormat.valueToString(new Date(123 * 1000L))) + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("client_id", "clientId") + .put("token_type", "Bearer") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForRefreshToken() throws ParseException { + + // given + OAuth2RefreshTokenEntity refreshToken = refreshToken(new Date(123 * 1000L), + oauth2AuthenticationWithUser(oauth2Request("clientId", scopes("foo", "bar")), "name")); + + UserInfo userInfo = userInfo("sub"); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(refreshToken, userInfo, authScopes); + + + // then + Map expected = new ImmutableMap.Builder() + .put("sub", "sub") + .put("exp", 123L) + .put("expires_at", dateFormat.valueToString(new Date(123 * 1000L))) + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("user_id", "name") + .put("client_id", "clientId") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForRefreshTokenWithoutUserInfo() throws ParseException { + + // given + OAuth2RefreshTokenEntity refreshToken = refreshToken(new Date(123 * 1000L), + oauth2AuthenticationWithUser(oauth2Request("clientId", scopes("foo", "bar")), "name")); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(refreshToken, null, authScopes); + + + // then + Map expected = new ImmutableMap.Builder() + .put("sub", "name") + .put("exp", 123L) + .put("expires_at", dateFormat.valueToString(new Date(123 * 1000L))) + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("user_id", "name") + .put("client_id", "clientId") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForRefreshTokenWithoutExpiry() { + + // given + OAuth2RefreshTokenEntity refreshToken = refreshToken(null, + oauth2AuthenticationWithUser(oauth2Request("clientId", scopes("foo", "bar")), "name")); + + UserInfo userInfo = userInfo("sub"); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(refreshToken, userInfo, authScopes); + + + // then + Map expected = new ImmutableMap.Builder() + .put("sub", "sub") + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("user_id", "name") + .put("client_id", "clientId") + .build(); + assertThat(result, is(equalTo(expected))); + } + + @Test + public void shouldAssembleExpectedResultForRefreshTokenWithoutUserAuthentication() throws ParseException { + // given + OAuth2RefreshTokenEntity refreshToken = refreshToken(null, + oauth2Authentication(oauth2Request("clientId", scopes("foo", "bar")), null)); + + Set authScopes = scopes("foo", "bar", "baz"); + + // when + Map result = assembler.assembleFrom(refreshToken, null, authScopes); + + + // then `user_id` should not be present + Map expected = new ImmutableMap.Builder() + .put("sub", "clientId") + .put("scope", "bar foo") + .put("active", Boolean.TRUE) + .put("client_id", "clientId") + .build(); + assertThat(result, is(equalTo(expected))); + } + + + + private UserInfo userInfo(String sub) { + UserInfo userInfo = mock(UserInfo.class); + given(userInfo.getSub()).willReturn(sub); + return userInfo; + } + + private OAuth2AccessTokenEntity accessToken(Date exp, Set scopes, Set permissions, String tokenType, OAuth2Authentication authentication) { + OAuth2AccessTokenEntity accessToken = mock(OAuth2AccessTokenEntity.class, RETURNS_DEEP_STUBS); + given(accessToken.getExpiration()).willReturn(exp); + given(accessToken.getScope()).willReturn(scopes); + given(accessToken.getPermissions()).willReturn(permissions); + given(accessToken.getTokenType()).willReturn(tokenType); + given(accessToken.getAuthenticationHolder().getAuthentication()).willReturn(authentication); + return accessToken; + } + + private OAuth2RefreshTokenEntity refreshToken(Date exp, OAuth2Authentication authentication) { + OAuth2RefreshTokenEntity refreshToken = mock(OAuth2RefreshTokenEntity.class, RETURNS_DEEP_STUBS); + given(refreshToken.getExpiration()).willReturn(exp); + given(refreshToken.getAuthenticationHolder().getAuthentication()).willReturn(authentication); + return refreshToken; + } + + private OAuth2Authentication oauth2AuthenticationWithUser(OAuth2Request request, String username) { + UsernamePasswordAuthenticationToken userAuthentication = new UsernamePasswordAuthenticationToken(username, "somepassword"); + return oauth2Authentication(request, userAuthentication); + } + + private OAuth2Authentication oauth2Authentication(OAuth2Request request, Authentication userAuthentication) { + return new OAuth2Authentication(request, userAuthentication); + } + + private OAuth2Request oauth2Request(String clientId) { + return oauth2Request(clientId, null); + } + + private OAuth2Request oauth2Request(String clientId, Set scopes) { + return new OAuth2Request(null, clientId, null, true, scopes, null, null, null, null); + } + + private Set scopes(String... scopes) { + return newHashSet(scopes); + } + + private Set permissions(Permission... permissions) { + return newHashSet(permissions); + } + + private Permission permission(Long resourceSetId, String... scopes) { + Permission permission = mock(Permission.class, RETURNS_DEEP_STUBS); + given(permission.getResourceSet().getId()).willReturn(resourceSetId); + given(permission.getScopes()).willReturn(scopes(scopes)); + return permission; + } +} diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ClientDetailsEntityService.java b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ClientDetailsEntityService.java index d99457170b..6ec31ca806 100644 --- a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ClientDetailsEntityService.java +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ClientDetailsEntityService.java @@ -1,49 +1,63 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.repository.OAuth2ClientRepository; import org.mitre.oauth2.repository.OAuth2TokenRepository; import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.mitre.openid.connect.model.WhitelistedSite; import org.mitre.openid.connect.service.ApprovedSiteService; import org.mitre.openid.connect.service.BlacklistedSiteService; import org.mitre.openid.connect.service.StatsService; import org.mitre.openid.connect.service.WhitelistedSiteService; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ResourceSetService; +import org.mockito.AdditionalAnswers; import org.mockito.InjectMocks; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import com.google.common.collect.Sets; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + /** * @author wkim * @@ -69,15 +83,69 @@ public class TestDefaultOAuth2ClientDetailsEntityService { @Mock private SystemScopeService scopeService; + @Mock + private ResourceSetService resourceSetService; + @Mock private StatsService statsService; + @Mock + private ConfigurationPropertiesBean config; + @InjectMocks private DefaultOAuth2ClientDetailsEntityService service; @Before public void prepare() { Mockito.reset(clientRepository, tokenRepository, approvedSiteService, whitelistedSiteService, blacklistedSiteService, scopeService, statsService); + + Mockito.when(clientRepository.saveClient(Matchers.any(ClientDetailsEntity.class))).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + return (ClientDetailsEntity) args[0]; + } + }); + + Mockito.when(clientRepository.updateClient(Matchers.anyLong(), Matchers.any(ClientDetailsEntity.class))).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + return (ClientDetailsEntity) args[1]; + } + }); + + Mockito.when(scopeService.fromStrings(Matchers.anySet())).thenAnswer(new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + Set input = (Set) args[0]; + Set output = new HashSet<>(); + for (String scope : input) { + output.add(new SystemScope(scope)); + } + return output; + } + }); + + Mockito.when(scopeService.toStrings(Matchers.anySet())).thenAnswer(new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + Set input = (Set) args[0]; + Set output = new HashSet<>(); + for (SystemScope scope : input) { + output.add(scope.getValue()); + } + return output; + } + }); + + // we're not testing reserved scopes here, just pass through when it's called + Mockito.when(scopeService.removeReservedScopes(Matchers.anySet())).then(AdditionalAnswers.returnsFirstArg()); + + Mockito.when(config.isHeartMode()).thenReturn(false); + } /** @@ -88,7 +156,7 @@ public void saveNewClient_badId() { // Set up a mock client. ClientDetailsEntity client = Mockito.mock(ClientDetailsEntity.class); - Mockito.when(client.getId()).thenReturn(12345L); // doesn't matter what id it returns + Mockito.when(client.getId()).thenReturn(12345L); // any non-null ID will work service.saveNewClient(client); } @@ -128,20 +196,15 @@ public void saveNewClient_idWasAssigned() { @Test public void saveNewClient_yesOfflineAccess() { - ClientDetailsEntity client = Mockito.mock(ClientDetailsEntity.class); - Mockito.when(client.getId()).thenReturn(null); - - Mockito.when(client.isAllowRefresh()).thenReturn(true); - - // scopes returned by client entities are Strings - @SuppressWarnings("unchecked") - Set scopes = Mockito.mock(Set.class); + ClientDetailsEntity client = new ClientDetailsEntity(); - Mockito.when(client.getScope()).thenReturn(scopes); + Set grantTypes = new HashSet<>(); + grantTypes.add("refresh_token"); + client.setGrantTypes(grantTypes); - service.saveNewClient(client); + client = service.saveNewClient(client); - Mockito.verify(scopes).add(SystemScopeService.OFFLINE_ACCESS); + assertThat(client.getScope().contains(SystemScopeService.OFFLINE_ACCESS), is(equalTo(true))); } /** @@ -150,20 +213,13 @@ public void saveNewClient_yesOfflineAccess() { @Test public void saveNewClient_noOfflineAccess() { - ClientDetailsEntity client = Mockito.mock(ClientDetailsEntity.class); - Mockito.when(client.getId()).thenReturn(null); + ClientDetailsEntity client = new ClientDetailsEntity(); - Mockito.when(client.isAllowRefresh()).thenReturn(false); + client = service.saveNewClient(client); - // scopes returned by client entities are Strings - @SuppressWarnings("unchecked") - Set scopes = Mockito.mock(Set.class); + Mockito.verify(scopeService, Mockito.atLeastOnce()).removeReservedScopes(Matchers.anySet()); - Mockito.when(client.getScope()).thenReturn(scopes); - - service.saveNewClient(client); - - Mockito.verify(scopes).remove(SystemScopeService.OFFLINE_ACCESS); + assertThat(client.getScope().contains(SystemScopeService.OFFLINE_ACCESS), is(equalTo(false))); } @Test @@ -223,6 +279,8 @@ public void deleteClient() { WhitelistedSite site = Mockito.mock(WhitelistedSite.class); Mockito.when(whitelistedSiteService.getByClientId(clientId)).thenReturn(site); + Mockito.when(resourceSetService.getAllForClient(client)).thenReturn(new HashSet()); + service.deleteClient(client); Mockito.verify(tokenRepository).clearTokensForClient(client); @@ -276,38 +334,299 @@ public void updateClient_blacklistedUri() { @Test public void updateClient_yesOfflineAccess() { - ClientDetailsEntity oldClient = Mockito.mock(ClientDetailsEntity.class); - ClientDetailsEntity newClient = Mockito.mock(ClientDetailsEntity.class); - - Mockito.when(newClient.isAllowRefresh()).thenReturn(true); + ClientDetailsEntity oldClient = new ClientDetailsEntity(); + ClientDetailsEntity client = new ClientDetailsEntity(); - // scopes returned by client entities are Strings - @SuppressWarnings("unchecked") - Set scopes = Mockito.mock(Set.class); + Set grantTypes = new HashSet<>(); + grantTypes.add("refresh_token"); + client.setGrantTypes(grantTypes); - Mockito.when(newClient.getScope()).thenReturn(scopes); + client = service.updateClient(oldClient, client); - service.updateClient(oldClient, newClient); + Mockito.verify(scopeService, Mockito.atLeastOnce()).removeReservedScopes(Matchers.anySet()); - Mockito.verify(scopes).add(SystemScopeService.OFFLINE_ACCESS); + assertThat(client.getScope().contains(SystemScopeService.OFFLINE_ACCESS), is(equalTo(true))); } @Test public void updateClient_noOfflineAccess() { - ClientDetailsEntity oldClient = Mockito.mock(ClientDetailsEntity.class); - ClientDetailsEntity newClient = Mockito.mock(ClientDetailsEntity.class); + ClientDetailsEntity oldClient = new ClientDetailsEntity(); - Mockito.when(newClient.isAllowRefresh()).thenReturn(false); + oldClient.getScope().add(SystemScopeService.OFFLINE_ACCESS); - // scopes returned by client entities are Strings - @SuppressWarnings("unchecked") - Set scopes = Mockito.mock(Set.class); + ClientDetailsEntity client = new ClientDetailsEntity(); - Mockito.when(newClient.getScope()).thenReturn(scopes); + client = service.updateClient(oldClient, client); - service.updateClient(oldClient, newClient); + Mockito.verify(scopeService, Mockito.atLeastOnce()).removeReservedScopes(Matchers.anySet()); + + assertThat(client.getScope().contains(SystemScopeService.OFFLINE_ACCESS), is(equalTo(false))); + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_authcode_invalidGrants() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + grantTypes.add("implicit"); + grantTypes.add("client_credentials"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("https://foo.bar/")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_implicit_invalidGrants() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("implicit"); + grantTypes.add("authorization_code"); + grantTypes.add("client_credentials"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.NONE); + + client.setRedirectUris(Sets.newHashSet("https://foo.bar/")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_clientcreds_invalidGrants() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("client_credentials"); + grantTypes.add("authorization_code"); + grantTypes.add("implicit"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_authcode_authMethod() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.SECRET_POST); + + client.setRedirectUris(Sets.newHashSet("https://foo.bar/")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_implicit_authMethod() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("implicit"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("https://foo.bar/")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_clientcreds_authMethod() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("client_credentials"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); + + client.setRedirectUris(Sets.newHashSet("https://foo.bar/")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_authcode_redirectUris() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_implicit_redirectUris() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("implicit"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.NONE); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_clientcreds_redirectUris() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("client_credentials"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("http://foo.bar/")); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_clientSecret() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("http://foo.bar/")); + + client.setClientSecret("secret!"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_noJwks() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("https://foo.bar/")); + + client.setJwks(null); + client.setJwksUri(null); + + service.saveNewClient(client); + + } + + @Test + public void heartMode_validAuthcodeClient() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + grantTypes.add("refresh_token"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("https://foo.bar/")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + assertThat(client.getClientId(), is(notNullValue(String.class))); + assertThat(client.getClientSecret(), is(nullValue())); + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_nonLocalHttpRedirect() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + grantTypes.add("refresh_token"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("http://foo.bar/")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); + + } + + @Test(expected = IllegalArgumentException.class) + public void heartMode_multipleRedirectClass() { + Mockito.when(config.isHeartMode()).thenReturn(true); + + ClientDetailsEntity client = new ClientDetailsEntity(); + Set grantTypes = new LinkedHashSet<>(); + grantTypes.add("authorization_code"); + grantTypes.add("refresh_token"); + client.setGrantTypes(grantTypes); + + client.setTokenEndpointAuthMethod(AuthMethod.PRIVATE_KEY); + + client.setRedirectUris(Sets.newHashSet("http://localhost/", "https://foo.bar", "foo://bar")); + + client.setJwksUri("https://foo.bar/jwks"); + + service.saveNewClient(client); - Mockito.verify(scopes).remove(SystemScopeService.OFFLINE_ACCESS); } } diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ProviderTokenService.java b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ProviderTokenService.java index a0e67a6252..afdb9dd6b0 100644 --- a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ProviderTokenService.java +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultOAuth2ProviderTokenService.java @@ -1,30 +1,24 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.util.Date; +import java.util.HashSet; import java.util.Set; import org.junit.Before; @@ -34,11 +28,11 @@ import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.repository.AuthenticationHolderRepository; import org.mitre.oauth2.repository.OAuth2TokenRepository; import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.SystemScopeService; -import org.mockito.AdditionalAnswers; import org.mockito.InjectMocks; import org.mockito.Matchers; import org.mockito.Mock; @@ -55,7 +49,26 @@ import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.security.oauth2.provider.token.TokenEnhancer; -import com.google.common.collect.Sets; +import static com.google.common.collect.Sets.newHashSet; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anySet; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @author wkim @@ -70,10 +83,14 @@ public class TestDefaultOAuth2ProviderTokenService { // Test Fixture: private OAuth2Authentication authentication; private ClientDetailsEntity client; + private ClientDetailsEntity badClient; private String clientId = "test_client"; - private Set scope = Sets.newHashSet("openid", "profile", "email", "offline_access"); + private String badClientId = "bad_client"; + private Set scope = newHashSet("openid", "profile", "email", "offline_access"); private OAuth2RefreshTokenEntity refreshToken; + private OAuth2AccessTokenEntity accessToken; private String refreshTokenValue = "refresh_token_value"; + private String userName = "6a50ac11786d402a9591d3e592ac770f"; private TokenRequest tokenRequest; // for use when refreshing access tokens @@ -105,69 +122,105 @@ public class TestDefaultOAuth2ProviderTokenService { */ @Before public void prepare() { - Mockito.reset(tokenRepository, authenticationHolderRepository, clientDetailsService, tokenEnhancer); - - + reset(tokenRepository, authenticationHolderRepository, clientDetailsService, tokenEnhancer); authentication = Mockito.mock(OAuth2Authentication.class); OAuth2Request clientAuth = new OAuth2Request(null, clientId, null, true, scope, null, null, null, null); - Mockito.when(authentication.getOAuth2Request()).thenReturn(clientAuth); + when(authentication.getOAuth2Request()).thenReturn(clientAuth); client = Mockito.mock(ClientDetailsEntity.class); - Mockito.when(client.getClientId()).thenReturn(clientId); - Mockito.when(clientDetailsService.loadClientByClientId(clientId)).thenReturn(client); + when(client.getClientId()).thenReturn(clientId); + when(clientDetailsService.loadClientByClientId(clientId)).thenReturn(client); + when(client.isReuseRefreshToken()).thenReturn(true); // by default in tests, allow refresh tokens - Mockito.when(client.isAllowRefresh()).thenReturn(true); + when(client.isAllowRefresh()).thenReturn(true); + + // by default, clear access tokens on refresh + when(client.isClearAccessTokensOnRefresh()).thenReturn(true); + + badClient = Mockito.mock(ClientDetailsEntity.class); + when(badClient.getClientId()).thenReturn(badClientId); + when(clientDetailsService.loadClientByClientId(badClientId)).thenReturn(badClient); refreshToken = Mockito.mock(OAuth2RefreshTokenEntity.class); - Mockito.when(tokenRepository.getRefreshTokenByValue(refreshTokenValue)).thenReturn(refreshToken); - Mockito.when(refreshToken.getClient()).thenReturn(client); - Mockito.when(refreshToken.isExpired()).thenReturn(false); + when(tokenRepository.getRefreshTokenByValue(refreshTokenValue)).thenReturn(refreshToken); + when(refreshToken.getClient()).thenReturn(client); + when(refreshToken.isExpired()).thenReturn(false); + + accessToken = Mockito.mock(OAuth2AccessTokenEntity.class); tokenRequest = new TokenRequest(null, clientId, null, null); storedAuthentication = authentication; storedAuthRequest = clientAuth; - storedAuthHolder = Mockito.mock(AuthenticationHolderEntity.class); - storedScope = Sets.newHashSet(scope); + storedAuthHolder = mock(AuthenticationHolderEntity.class); + storedScope = newHashSet(scope); - Mockito.when(refreshToken.getAuthenticationHolder()).thenReturn(storedAuthHolder); - Mockito.when(storedAuthHolder.getAuthentication()).thenReturn(storedAuthentication); - Mockito.when(storedAuthentication.getOAuth2Request()).thenReturn(storedAuthRequest); + when(refreshToken.getAuthenticationHolder()).thenReturn(storedAuthHolder); + when(storedAuthHolder.getAuthentication()).thenReturn(storedAuthentication); + when(storedAuthentication.getOAuth2Request()).thenReturn(storedAuthRequest); - Mockito.when(authenticationHolderRepository.save(Matchers.any(AuthenticationHolderEntity.class))).thenReturn(storedAuthHolder); + when(authenticationHolderRepository.save(any(AuthenticationHolderEntity.class))).thenReturn(storedAuthHolder); - Mockito.when(scopeService.removeRestrictedScopes(Matchers.anySet())).then(AdditionalAnswers.returnsFirstArg()); + when(scopeService.fromStrings(anySet())).thenAnswer(new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + Set input = (Set) args[0]; + Set output = new HashSet<>(); + for (String scope : input) { + output.add(new SystemScope(scope)); + } + return output; + } + }); - Mockito.when(tokenEnhancer.enhance(Matchers.any(OAuth2AccessTokenEntity.class), Matchers.any(OAuth2Authentication.class))) - .thenAnswer(new Answer(){ - @Override + when(scopeService.toStrings(anySet())).thenAnswer(new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + Set input = (Set) args[0]; + Set output = new HashSet<>(); + for (SystemScope scope : input) { + output.add(scope.getValue()); + } + return output; + } + }); + + // we're not testing restricted or reserved scopes here, just pass through + when(scopeService.removeReservedScopes(anySet())).then(returnsFirstArg()); + when(scopeService.removeRestrictedAndReservedScopes(anySet())).then(returnsFirstArg()); + + when(tokenEnhancer.enhance(any(OAuth2AccessTokenEntity.class), any(OAuth2Authentication.class))) + .thenAnswer(new Answer(){ + @Override public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); return (OAuth2AccessTokenEntity) args[0]; } }); - Mockito.when(tokenRepository.saveAccessToken(Matchers.any(OAuth2AccessTokenEntity.class))) - .thenAnswer(new Answer() { - @Override - public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - return (OAuth2AccessTokenEntity) args[0]; - } - - }); - - Mockito.when(tokenRepository.saveRefreshToken(Matchers.any(OAuth2RefreshTokenEntity.class))) - .thenAnswer(new Answer() { - @Override - public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - return (OAuth2RefreshTokenEntity) args[0]; - } - }); - + when(tokenRepository.saveAccessToken(any(OAuth2AccessTokenEntity.class))) + .thenAnswer(new Answer() { + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + return (OAuth2AccessTokenEntity) args[0]; + } + + }); + + when(tokenRepository.saveRefreshToken(any(OAuth2RefreshTokenEntity.class))) + .thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + return (OAuth2RefreshTokenEntity) args[0]; + } + }); + } /** @@ -175,8 +228,7 @@ public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throw */ @Test public void createAccessToken_nullAuth() { - - Mockito.when(authentication.getOAuth2Request()).thenReturn(null); + when(authentication.getOAuth2Request()).thenReturn(null); try { service.createAccessToken(null); @@ -198,8 +250,7 @@ public void createAccessToken_nullAuth() { */ @Test(expected = InvalidClientException.class) public void createAccessToken_nullClient() { - - Mockito.when(clientDetailsService.loadClientByClientId(Matchers.anyString())).thenReturn(null); + when(clientDetailsService.loadClientByClientId(anyString())).thenReturn(null); service.createAccessToken(authentication); } @@ -209,17 +260,18 @@ public void createAccessToken_nullClient() { */ @Test public void createAccessToken_noRefresh() { - - Mockito.when(client.isAllowRefresh()).thenReturn(false); + when(client.isAllowRefresh()).thenReturn(false); OAuth2AccessTokenEntity token = service.createAccessToken(authentication); - Mockito.verify(clientDetailsService).loadClientByClientId(Matchers.anyString()); - Mockito.verify(authenticationHolderRepository).save(Matchers.any(AuthenticationHolderEntity.class)); - Mockito.verify(tokenEnhancer).enhance(Matchers.any(OAuth2AccessTokenEntity.class), Mockito.eq(authentication)); - Mockito.verify(tokenRepository).saveAccessToken(Matchers.any(OAuth2AccessTokenEntity.class)); + verify(clientDetailsService).loadClientByClientId(anyString()); + verify(authenticationHolderRepository).save(any(AuthenticationHolderEntity.class)); + verify(tokenEnhancer).enhance(any(OAuth2AccessTokenEntity.class), Matchers.eq(authentication)); + verify(tokenRepository).saveAccessToken(any(OAuth2AccessTokenEntity.class)); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + + verify(tokenRepository, Mockito.never()).saveRefreshToken(any(OAuth2RefreshTokenEntity.class)); - Mockito.verify(tokenRepository, Mockito.never()).saveRefreshToken(Matchers.any(OAuth2RefreshTokenEntity.class)); assertThat(token.getRefreshToken(), is(nullValue())); } @@ -228,17 +280,17 @@ public void createAccessToken_noRefresh() { */ @Test public void createAccessToken_yesRefresh() { - - OAuth2Request clientAuth = new OAuth2Request(null, clientId, null, true, Sets.newHashSet(SystemScopeService.OFFLINE_ACCESS), null, null, null, null); - Mockito.when(authentication.getOAuth2Request()).thenReturn(clientAuth); - Mockito.when(client.isAllowRefresh()).thenReturn(true); + OAuth2Request clientAuth = new OAuth2Request(null, clientId, null, true, newHashSet(SystemScopeService.OFFLINE_ACCESS), null, null, null, null); + when(authentication.getOAuth2Request()).thenReturn(clientAuth); + when(client.isAllowRefresh()).thenReturn(true); OAuth2AccessTokenEntity token = service.createAccessToken(authentication); // Note: a refactor may be appropriate to only save refresh tokens once to the repository during creation. - Mockito.verify(tokenRepository, Mockito.atLeastOnce()).saveRefreshToken(Matchers.any(OAuth2RefreshTokenEntity.class)); - assertThat(token.getRefreshToken(), is(notNullValue())); + verify(tokenRepository, atLeastOnce()).saveRefreshToken(any(OAuth2RefreshTokenEntity.class)); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertThat(token.getRefreshToken(), is(notNullValue())); } /** @@ -246,12 +298,11 @@ public void createAccessToken_yesRefresh() { */ @Test public void createAccessToken_expiration() { - Integer accessTokenValiditySeconds = 3600; Integer refreshTokenValiditySeconds = 600; - Mockito.when(client.getAccessTokenValiditySeconds()).thenReturn(accessTokenValiditySeconds); - Mockito.when(client.getRefreshTokenValiditySeconds()).thenReturn(refreshTokenValiditySeconds); + when(client.getAccessTokenValiditySeconds()).thenReturn(accessTokenValiditySeconds); + when(client.getRefreshTokenValiditySeconds()).thenReturn(refreshTokenValiditySeconds); long start = System.currentTimeMillis(); OAuth2AccessTokenEntity token = service.createAccessToken(authentication); @@ -263,103 +314,150 @@ public void createAccessToken_expiration() { Date lowerBoundRefreshTokens = new Date(start + (refreshTokenValiditySeconds * 1000L) - DELTA); Date upperBoundRefreshTokens = new Date(end + (refreshTokenValiditySeconds * 1000L) + DELTA); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertTrue(token.getExpiration().after(lowerBoundAccessTokens) && token.getExpiration().before(upperBoundAccessTokens)); assertTrue(token.getRefreshToken().getExpiration().after(lowerBoundRefreshTokens) && token.getRefreshToken().getExpiration().before(upperBoundRefreshTokens)); } @Test public void createAccessToken_checkClient() { - OAuth2AccessTokenEntity token = service.createAccessToken(authentication); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertThat(token.getClient().getClientId(), equalTo(clientId)); } @Test public void createAccessToken_checkScopes() { - OAuth2AccessTokenEntity token = service.createAccessToken(authentication); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertThat(token.getScope(), equalTo(scope)); } @Test public void createAccessToken_checkAttachedAuthentication() { + AuthenticationHolderEntity authHolder = mock(AuthenticationHolderEntity.class); + when(authHolder.getAuthentication()).thenReturn(authentication); - AuthenticationHolderEntity authHolder = Mockito.mock(AuthenticationHolderEntity.class); - Mockito.when(authHolder.getAuthentication()).thenReturn(authentication); - - Mockito.when(authenticationHolderRepository.save(Matchers.any(AuthenticationHolderEntity.class))).thenReturn(authHolder); + when(authenticationHolderRepository.save(any(AuthenticationHolderEntity.class))).thenReturn(authHolder); OAuth2AccessTokenEntity token = service.createAccessToken(authentication); assertThat(token.getAuthenticationHolder().getAuthentication(), equalTo(authentication)); - Mockito.verify(authenticationHolderRepository).save(Matchers.any(AuthenticationHolderEntity.class)); + verify(authenticationHolderRepository).save(any(AuthenticationHolderEntity.class)); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); } @Test(expected = InvalidTokenException.class) public void refreshAccessToken_noRefreshToken() { - - Mockito.when(tokenRepository.getRefreshTokenByValue(Matchers.anyString())).thenReturn(null); + when(tokenRepository.getRefreshTokenByValue(anyString())).thenReturn(null); service.refreshAccessToken(refreshTokenValue, tokenRequest); } @Test(expected = InvalidClientException.class) public void refreshAccessToken_notAllowRefresh() { + when(client.isAllowRefresh()).thenReturn(false); - Mockito.when(client.isAllowRefresh()).thenReturn(false); + service.refreshAccessToken(refreshTokenValue, tokenRequest); + } + + @Test(expected = InvalidClientException.class) + public void refreshAccessToken_clientMismatch() { + tokenRequest = new TokenRequest(null, badClientId, null, null); service.refreshAccessToken(refreshTokenValue, tokenRequest); } @Test(expected = InvalidTokenException.class) public void refreshAccessToken_expired() { - - Mockito.when(refreshToken.isExpired()).thenReturn(true); + when(refreshToken.isExpired()).thenReturn(true); service.refreshAccessToken(refreshTokenValue, tokenRequest); } @Test public void refreshAccessToken_verifyAcessToken() { + OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); + + verify(tokenRepository).clearAccessTokensForRefreshToken(refreshToken); + + assertThat(token.getClient(), equalTo(client)); + assertThat(token.getRefreshToken(), equalTo(refreshToken)); + assertThat(token.getAuthenticationHolder(), equalTo(storedAuthHolder)); + + verify(tokenEnhancer).enhance(token, storedAuthentication); + verify(tokenRepository).saveAccessToken(token); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + + } + + @Test + public void refreshAccessToken_rotateRefreshToken() { + when(client.isReuseRefreshToken()).thenReturn(false); OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); - Mockito.verify(tokenRepository).clearAccessTokensForRefreshToken(refreshToken); + verify(tokenRepository).clearAccessTokensForRefreshToken(refreshToken); + + assertThat(token.getClient(), equalTo(client)); + assertThat(token.getRefreshToken(), not(equalTo(refreshToken))); + assertThat(token.getAuthenticationHolder(), equalTo(storedAuthHolder)); + + verify(tokenEnhancer).enhance(token, storedAuthentication); + verify(tokenRepository).saveAccessToken(token); + verify(tokenRepository).removeRefreshToken(refreshToken); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + + } + + @Test + public void refreshAccessToken_keepAccessTokens() { + when(client.isClearAccessTokensOnRefresh()).thenReturn(false); + + OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); + + verify(tokenRepository, never()).clearAccessTokensForRefreshToken(refreshToken); assertThat(token.getClient(), equalTo(client)); assertThat(token.getRefreshToken(), equalTo(refreshToken)); assertThat(token.getAuthenticationHolder(), equalTo(storedAuthHolder)); - Mockito.verify(tokenEnhancer).enhance(token, storedAuthentication); - Mockito.verify(tokenRepository).saveAccessToken(token); + verify(tokenEnhancer).enhance(token, storedAuthentication); + verify(tokenRepository).saveAccessToken(token); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + } @Test public void refreshAccessToken_requestingSameScope() { - OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertThat(token.getScope(), equalTo(storedScope)); } @Test public void refreshAccessToken_requestingLessScope() { - - Set lessScope = Sets.newHashSet("openid", "profile"); + Set lessScope = newHashSet("openid", "profile"); tokenRequest.setScope(lessScope); OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertThat(token.getScope(), equalTo(lessScope)); } @Test(expected = InvalidScopeException.class) public void refreshAccessToken_requestingMoreScope() { - - Set moreScope = Sets.newHashSet(storedScope); + Set moreScope = newHashSet(storedScope); moreScope.add("address"); moreScope.add("phone"); @@ -374,8 +472,7 @@ public void refreshAccessToken_requestingMoreScope() { */ @Test(expected = InvalidScopeException.class) public void refreshAccessToken_requestingMixedScope() { - - Set mixedScope = Sets.newHashSet("openid", "profile", "address", "phone"); // no email or offline_access + Set mixedScope = newHashSet("openid", "profile", "address", "phone"); // no email or offline_access tokenRequest.setScope(mixedScope); @@ -384,23 +481,25 @@ public void refreshAccessToken_requestingMixedScope() { @Test public void refreshAccessToken_requestingEmptyScope() { - - Set emptyScope = Sets.newHashSet(); + Set emptyScope = newHashSet(); tokenRequest.setScope(emptyScope); OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertThat(token.getScope(), equalTo(storedScope)); } @Test public void refreshAccessToken_requestingNullScope() { - tokenRequest.setScope(null); OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertThat(token.getScope(), equalTo(storedScope)); } @@ -410,10 +509,9 @@ public void refreshAccessToken_requestingNullScope() { */ @Test public void refreshAccessToken_expiration() { - Integer accessTokenValiditySeconds = 3600; - Mockito.when(client.getAccessTokenValiditySeconds()).thenReturn(accessTokenValiditySeconds); + when(client.getAccessTokenValiditySeconds()).thenReturn(accessTokenValiditySeconds); long start = System.currentTimeMillis(); OAuth2AccessTokenEntity token = service.refreshAccessToken(refreshTokenValue, tokenRequest); @@ -423,7 +521,26 @@ public void refreshAccessToken_expiration() { Date lowerBoundAccessTokens = new Date(start + (accessTokenValiditySeconds * 1000L) - DELTA); Date upperBoundAccessTokens = new Date(end + (accessTokenValiditySeconds * 1000L) + DELTA); + verify(scopeService, atLeastOnce()).removeReservedScopes(anySet()); + assertTrue(token.getExpiration().after(lowerBoundAccessTokens) && token.getExpiration().before(upperBoundAccessTokens)); } - + + @Test + public void getAllAccessTokensForUser(){ + when(tokenRepository.getAccessTokensByUserName(userName)).thenReturn(newHashSet(accessToken)); + + Set tokens = service.getAllAccessTokensForUser(userName); + assertEquals(1, tokens.size()); + assertTrue(tokens.contains(accessToken)); + } + + @Test + public void getAllRefreshTokensForUser(){ + when(tokenRepository.getRefreshTokensByUserName(userName)).thenReturn(newHashSet(refreshToken)); + + Set tokens = service.getAllRefreshTokensForUser(userName); + assertEquals(1, tokens.size()); + assertTrue(tokens.contains(refreshToken)); + } } diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultSystemScopeService.java b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultSystemScopeService.java index ae1f7761eb..808d96b581 100644 --- a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultSystemScopeService.java +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultSystemScopeService.java @@ -1,26 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.oauth2.service.impl; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; - import java.util.Set; import org.junit.Before; @@ -31,12 +27,16 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import com.google.common.collect.Sets; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; + +import static org.junit.Assert.assertThat; + /** * @author wkim * @@ -50,21 +50,19 @@ public class TestDefaultSystemScopeService { private SystemScope defaultScope1; private SystemScope defaultScope2; private SystemScope dynScope1; - private SystemScope extraScope1; - private SystemScope structuredScope1; - private SystemScope structuredScope1Value; + private SystemScope restrictedScope1; private String defaultDynScope1String = "defaultDynScope1"; private String defaultDynScope2String = "defaultDynScope2"; private String defaultScope1String = "defaultScope1"; private String defaultScope2String = "defaultScope2"; private String dynScope1String = "dynScope1"; - private String extraScope1String = "extraScope1"; - private String structuredScope1String = "structuredScope1"; - private String structuredValue = "structuredValue"; + private String restrictedScope1String = "restrictedScope1"; private Set allScopes; private Set allScopeStrings; + private Set allScopesWithValue; + private Set allScopeStringsWithValue; @Mock private SystemScopeRepository repository; @@ -80,55 +78,40 @@ public void prepare() { Mockito.reset(repository); - // two default and dynamically registerable scopes + // two default and dynamically registerable scopes (unrestricted) defaultDynScope1 = new SystemScope(defaultDynScope1String); defaultDynScope2 = new SystemScope(defaultDynScope2String); - defaultDynScope1.setAllowDynReg(true); - defaultDynScope2.setAllowDynReg(true); defaultDynScope1.setDefaultScope(true); defaultDynScope2.setDefaultScope(true); - // two strictly default scopes (isAllowDynReg false) + // two strictly default scopes (restricted) defaultScope1 = new SystemScope(defaultScope1String); defaultScope2 = new SystemScope(defaultScope2String); + defaultScope1.setRestricted(true); + defaultScope2.setRestricted(true); defaultScope1.setDefaultScope(true); defaultScope2.setDefaultScope(true); // one strictly dynamically registerable scope (isDefault false) dynScope1 = new SystemScope(dynScope1String); - dynScope1.setAllowDynReg(true); - // extraScope1 : extra scope that is neither (defaults to false/false) - extraScope1 = new SystemScope(extraScope1String); + // extraScope1 : extra scope that is neither restricted nor default (defaults to false/false) + restrictedScope1 = new SystemScope(restrictedScope1String); + restrictedScope1.setRestricted(true); - // structuredScope1 : structured scope - structuredScope1 = new SystemScope(structuredScope1String); - structuredScope1.setStructured(true); - // structuredScope1Value : structured scope with value - structuredScope1Value = new SystemScope(structuredScope1String); - structuredScope1Value.setStructured(true); - structuredScope1Value.setStructuredValue(structuredValue); + allScopes = Sets.newHashSet(defaultDynScope1, defaultDynScope2, defaultScope1, defaultScope2, dynScope1, restrictedScope1); + allScopeStrings = Sets.newHashSet(defaultDynScope1String, defaultDynScope2String, defaultScope1String, defaultScope2String, dynScope1String, restrictedScope1String); - allScopes = Sets.newHashSet(defaultDynScope1, defaultDynScope2, defaultScope1, defaultScope2, dynScope1, extraScope1, structuredScope1, structuredScope1Value); - allScopeStrings = Sets.newHashSet(defaultDynScope1String, defaultDynScope2String, defaultScope1String, defaultScope2String, dynScope1String, extraScope1String, structuredScope1String, structuredScope1String + ":" + structuredValue); + allScopesWithValue = Sets.newHashSet(defaultDynScope1, defaultDynScope2, defaultScope1, defaultScope2, dynScope1, restrictedScope1); + allScopeStringsWithValue = Sets.newHashSet(defaultDynScope1String, defaultDynScope2String, defaultScope1String, defaultScope2String, dynScope1String, restrictedScope1String); Mockito.when(repository.getByValue(defaultDynScope1String)).thenReturn(defaultDynScope1); Mockito.when(repository.getByValue(defaultDynScope2String)).thenReturn(defaultDynScope2); Mockito.when(repository.getByValue(defaultScope1String)).thenReturn(defaultScope1); Mockito.when(repository.getByValue(defaultScope2String)).thenReturn(defaultScope2); Mockito.when(repository.getByValue(dynScope1String)).thenReturn(dynScope1); - Mockito.when(repository.getByValue(extraScope1String)).thenReturn(extraScope1); - // we re-use this value so we've got to use thenAnswer instead - Mockito.when(repository.getByValue(structuredScope1String)).thenAnswer(new Answer() { - @Override - public SystemScope answer(InvocationOnMock invocation) throws Throwable { - SystemScope s = new SystemScope(structuredScope1String); - s.setStructured(true); - return s; - } - - }); + Mockito.when(repository.getByValue(restrictedScope1String)).thenReturn(restrictedScope1); Mockito.when(repository.getAll()).thenReturn(allScopes); } @@ -148,11 +131,19 @@ public void getDefaults() { } @Test - public void getDynReg() { + public void getUnrestricted() { - Set dynReg = Sets.newHashSet(defaultDynScope1, defaultDynScope2, dynScope1); + Set unrestricted = Sets.newHashSet(defaultDynScope1, defaultDynScope2, dynScope1); + + assertThat(service.getUnrestricted(), equalTo(unrestricted)); + } + + @Test + public void getRestricted() { + Set restricted = Sets.newHashSet(defaultScope1, defaultScope2, restrictedScope1); + + assertThat(service.getRestricted(), equalTo(restricted)); - assertThat(service.getDynReg(), equalTo(dynReg)); } @Test @@ -162,6 +153,8 @@ public void fromStrings() { assertThat(service.fromStrings(null), is(nullValue())); assertThat(service.fromStrings(allScopeStrings), equalTo(allScopes)); + + assertThat(service.fromStrings(allScopeStringsWithValue), equalTo(allScopesWithValue)); } @Test @@ -171,6 +164,8 @@ public void toStrings() { assertThat(service.toStrings(null), is(nullValue())); assertThat(service.toStrings(allScopes), equalTo(allScopeStrings)); + + assertThat(service.toStrings(allScopesWithValue), equalTo(allScopeStringsWithValue)); } @Test @@ -191,25 +186,4 @@ public void scopesMatch() { assertThat(service.scopesMatch(expected, actualBad), is(false)); } - @Test - public void scopesMatch_structured() { - Set expected = Sets.newHashSet("foo", "bar", "baz"); - Set actualGood = Sets.newHashSet("foo:value", "baz", "bar"); - Set actualBad = Sets.newHashSet("foo:value", "bar:value"); - - // note: we have to use "thenAnswer" here to mimic the repository not serializing the structuredValue field - Mockito.when(repository.getByValue("foo")).thenAnswer(new Answer() { - @Override - public SystemScope answer(InvocationOnMock invocation) throws Throwable { - SystemScope foo = new SystemScope("foo"); - foo.setStructured(true); - return foo; - } - - }); - - assertThat(service.scopesMatch(expected, actualGood), is(true)); - - assertThat(service.scopesMatch(expected, actualBad), is(false)); - } } diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/assertion/TestJWTBearerAuthenticationProvider.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/assertion/TestJWTBearerAuthenticationProvider.java new file mode 100644 index 0000000000..fde99f499e --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/assertion/TestJWTBearerAuthenticationProvider.java @@ -0,0 +1,414 @@ +package org.mitre.openid.connect.assertion; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.ClientKeyCacheService; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWEHeader; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.EncryptedJWT; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; +import com.nimbusds.jwt.SignedJWT; + +@RunWith(MockitoJUnitRunner.class) +public class TestJWTBearerAuthenticationProvider { + + private static final String CLIENT_ID = "client"; + private static final String SUBJECT = "subject"; + + @Mock + private ClientKeyCacheService validators; + @Mock + private ClientDetailsEntityService clientService; + @Mock + private ConfigurationPropertiesBean config; + + @InjectMocks + private JWTBearerAuthenticationProvider jwtBearerAuthenticationProvider; + + @Mock + private JWTBearerAssertionAuthenticationToken token; + @Mock + private ClientDetailsEntity client; + @Mock + private JWTSigningAndValidationService validator; + + private GrantedAuthority authority1 = new SimpleGrantedAuthority("1"); + private GrantedAuthority authority2 = new SimpleGrantedAuthority("2"); + private GrantedAuthority authority3 = new SimpleGrantedAuthority("3"); + + @Before + public void setup() { + when(clientService.loadClientByClientId(CLIENT_ID)).thenReturn(client); + + when(token.getName()).thenReturn(CLIENT_ID); + + when(client.getClientId()).thenReturn(CLIENT_ID); + when(client.getTokenEndpointAuthMethod()).thenReturn(AuthMethod.NONE); + when(client.getAuthorities()).thenReturn(ImmutableSet.of(authority1, authority2, authority3)); + + when(validators.getValidator(client, JWSAlgorithm.RS256)).thenReturn(validator); + when(validator.validateSignature(any(SignedJWT.class))).thenReturn(true); + + when(config.getIssuer()).thenReturn("http://issuer.com/"); + } + + @Test + public void should_not_support_UsernamePasswordAuthenticationToken() { + assertThat(jwtBearerAuthenticationProvider.supports(UsernamePasswordAuthenticationToken.class), is(false)); + } + + @Test + public void should_support_JWTBearerAssertionAuthenticationToken() { + assertThat(jwtBearerAuthenticationProvider.supports(JWTBearerAssertionAuthenticationToken.class), is(true)); + } + + @Test + public void should_throw_UsernameNotFoundException_when_clientService_throws_InvalidClientException() { + when(clientService.loadClientByClientId(CLIENT_ID)).thenThrow(new InvalidClientException("invalid client")); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(UsernameNotFoundException.class)); + assertThat(thrown.getMessage(), is("Could not find client: " + CLIENT_ID)); + } + + @Test + public void should_throw_AuthenticationServiceException_for_PlainJWT() { + mockPlainJWTAuthAttempt(); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("Unsupported JWT type: " + PlainJWT.class.getName())); + } + + @Test + public void should_throw_AuthenticationServiceException_for_EncryptedJWT() { + mockEncryptedJWTAuthAttempt(); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("Unsupported JWT type: " + EncryptedJWT.class.getName())); + } + + @Test + public void should_throw_AuthenticationServiceException_for_SignedJWT_when_signing_algorithms_do_not_match() { + when(client.getTokenEndpointAuthSigningAlg()).thenReturn(JWSAlgorithm.RS256); + SignedJWT signedJWT = createSignedJWT(JWSAlgorithm.ES384); + when(token.getJwt()).thenReturn(signedJWT); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("Client's registered token endpoint signing algorithm (RS256) does not match token's actual algorithm (ES384)")); + } + + @Test + public void should_throw_AuthenticationServiceException_for_SignedJWT_when_unsupported_authentication_method_for_SignedJWT() { + List unsupportedAuthMethods = + Arrays.asList(null, AuthMethod.NONE, AuthMethod.SECRET_BASIC, AuthMethod.SECRET_POST); + + for (AuthMethod unsupportedAuthMethod : unsupportedAuthMethods) { + SignedJWT signedJWT = createSignedJWT(); + when(token.getJwt()).thenReturn(signedJWT); + when(client.getTokenEndpointAuthMethod()).thenReturn(unsupportedAuthMethod); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("Client does not support this authentication method.")); + } + } + + @Test + public void should_throw_AuthenticationServiceException_for_SignedJWT_when_invalid_algorithm_for_PRIVATE_KEY_auth_method() { + List invalidAlgorithms = Arrays.asList(JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512); + + for (JWSAlgorithm algorithm : invalidAlgorithms) { + SignedJWT signedJWT = createSignedJWT(algorithm); + when(token.getJwt()).thenReturn(signedJWT); + when(client.getTokenEndpointAuthMethod()).thenReturn(AuthMethod.PRIVATE_KEY); + when(client.getTokenEndpointAuthSigningAlg()).thenReturn(algorithm); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Unable to create signature validator for method")); + } + } + + @Test + public void should_throw_AuthenticationServiceException_for_SignedJWT_when_invalid_algorithm_for_SECRET_JWT_auth_method() { + List invalidAlgorithms = Arrays.asList( + JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512, + JWSAlgorithm.ES256, JWSAlgorithm.ES384, JWSAlgorithm.ES512, + JWSAlgorithm.PS256, JWSAlgorithm.PS384, JWSAlgorithm.PS512); + + for (JWSAlgorithm algorithm : invalidAlgorithms) { + SignedJWT signedJWT = createSignedJWT(algorithm); + when(token.getJwt()).thenReturn(signedJWT); + when(client.getTokenEndpointAuthMethod()).thenReturn(AuthMethod.SECRET_JWT); + when(client.getTokenEndpointAuthSigningAlg()).thenReturn(algorithm); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Unable to create signature validator for method")); + } + } + + @Test + public void should_throw_AuthenticationServiceException_for_SignedJWT_when_in_heart_mode_and_auth_method_is_not_PRIVATE_KEY() { + SignedJWT signedJWT = createSignedJWT(JWSAlgorithm.HS256); + when(token.getJwt()).thenReturn(signedJWT); + when(client.getTokenEndpointAuthSigningAlg()).thenReturn(JWSAlgorithm.HS256); + when(config.isHeartMode()).thenReturn(true); + when(client.getTokenEndpointAuthMethod()).thenReturn(AuthMethod.SECRET_JWT); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("[HEART mode] Invalid authentication method")); + } + + @Test + public void should_throw_AuthenticationServiceException_for_SignedJWT_when_null_validator() { + mockSignedJWTAuthAttempt(); + when(validators.getValidator(any(ClientDetailsEntity.class), any(JWSAlgorithm.class))).thenReturn(null); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Unable to create signature validator for client")); + } + + @Test + public void should_throw_AuthenticationServiceException_for_SignedJWT_when_invalid_signature() { + SignedJWT signedJWT = mockSignedJWTAuthAttempt(); + when(validator.validateSignature(signedJWT)).thenReturn(false); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("Signature did not validate for presented JWT authentication.")); + } + + @Test + public void should_throw_AuthenticationServiceException_when_null_issuer() { + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().issuer(null).build(); + mockSignedJWTAuthAttempt(jwtClaimsSet); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("Assertion Token Issuer is null")); + } + + @Test + public void should_throw_AuthenticationServiceException_when_not_matching_issuer() { + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().issuer("not matching").build(); + mockSignedJWTAuthAttempt(jwtClaimsSet); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Issuers do not match")); + } + + @Test + public void should_throw_AuthenticationServiceException_when_null_expiration_time() { + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().issuer(CLIENT_ID).expirationTime(null).build(); + mockSignedJWTAuthAttempt(jwtClaimsSet); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), is("Assertion Token does not have required expiration claim")); + } + + @Test + public void should_throw_AuthenticationServiceException_when_expired_jwt() { + Date expiredDate = new Date(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(500)); + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().issuer(CLIENT_ID).expirationTime(expiredDate).build(); + mockSignedJWTAuthAttempt(jwtClaimsSet); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Assertion Token is expired")); + } + + @Test + public void should_throw_AuthenticationServiceException_when_jwt_valid_in_future() { + Date futureDate = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(500)); + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().issuer(CLIENT_ID).expirationTime(futureDate).notBeforeTime(futureDate).build(); + mockSignedJWTAuthAttempt(jwtClaimsSet); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Assertion Token not valid until")); + } + + @Test + public void should_throw_AuthenticationServiceException_when_jwt_issued_in_future() { + Date futureDate = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(500)); + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().issuer(CLIENT_ID).expirationTime(futureDate).issueTime(futureDate).build(); + mockSignedJWTAuthAttempt(jwtClaimsSet); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Assertion Token was issued in the future")); + } + + @Test + public void should_throw_AuthenticationServiceException_when_unmatching_audience() { + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().issuer(CLIENT_ID).expirationTime(new Date()).audience("invalid").build(); + mockSignedJWTAuthAttempt(jwtClaimsSet); + + Throwable thrown = authenticateAndReturnThrownException(); + + assertThat(thrown, instanceOf(AuthenticationServiceException.class)); + assertThat(thrown.getMessage(), startsWith("Audience does not match")); + } + + @Test + public void should_return_valid_token_when_audience_contains_token_endpoint() { + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .issuer(CLIENT_ID) + .subject(SUBJECT) + .expirationTime(new Date()) + .audience(ImmutableList.of("http://issuer.com/token", "invalid")) + .build(); + JWT jwt = mockSignedJWTAuthAttempt(jwtClaimsSet); + + Authentication authentication = jwtBearerAuthenticationProvider.authenticate(token); + + assertThat(authentication, instanceOf(JWTBearerAssertionAuthenticationToken.class)); + + JWTBearerAssertionAuthenticationToken token = (JWTBearerAssertionAuthenticationToken) authentication; + assertThat(token.getName(), is(SUBJECT)); + assertThat(token.getJwt(), is(jwt)); + assertThat(token.getAuthorities(), hasItems(authority1, authority2, authority3)); + assertThat(token.getAuthorities().size(), is(4)); + } + + @Test + public void should_return_valid_token_when_issuer_does_not_end_with_slash_and_audience_contains_token_endpoint() { + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .issuer(CLIENT_ID) + .subject(SUBJECT) + .expirationTime(new Date()) + .audience(ImmutableList.of("http://issuer.com/token")) + .build(); + JWT jwt = mockSignedJWTAuthAttempt(jwtClaimsSet); + when(config.getIssuer()).thenReturn("http://issuer.com/"); + + Authentication authentication = jwtBearerAuthenticationProvider.authenticate(token); + + assertThat(authentication, instanceOf(JWTBearerAssertionAuthenticationToken.class)); + + JWTBearerAssertionAuthenticationToken token = (JWTBearerAssertionAuthenticationToken) authentication; + assertThat(token.getName(), is(SUBJECT)); + assertThat(token.getJwt(), is(jwt)); + assertThat(token.getAuthorities(), hasItems(authority1, authority2, authority3)); + assertThat(token.getAuthorities().size(), is(4)); + } + + private void mockPlainJWTAuthAttempt() { + PlainJWT plainJWT = new PlainJWT(createJwtClaimsSet()); + when(token.getJwt()).thenReturn(plainJWT); + } + + private void mockEncryptedJWTAuthAttempt() { + JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.A128GCMKW, EncryptionMethod.A256GCM).build(); + EncryptedJWT encryptedJWT = new EncryptedJWT(jweHeader, createJwtClaimsSet()); + when(token.getJwt()).thenReturn(encryptedJWT); + } + + private SignedJWT mockSignedJWTAuthAttempt() { + return mockSignedJWTAuthAttempt(createJwtClaimsSet()); + } + + private SignedJWT mockSignedJWTAuthAttempt(JWTClaimsSet jwtClaimsSet) { + SignedJWT signedJWT = createSignedJWT(JWSAlgorithm.RS256, jwtClaimsSet); + when(token.getJwt()).thenReturn(signedJWT); + when(client.getTokenEndpointAuthMethod()).thenReturn(AuthMethod.PRIVATE_KEY); + when(client.getTokenEndpointAuthSigningAlg()).thenReturn(JWSAlgorithm.RS256); + return signedJWT; + } + + private Throwable authenticateAndReturnThrownException() { + try { + jwtBearerAuthenticationProvider.authenticate(token); + } catch (Throwable throwable) { + return throwable; + } + throw new AssertionError("No exception thrown when expected"); + } + + private SignedJWT createSignedJWT() { + return createSignedJWT(JWSAlgorithm.RS256); + } + + private SignedJWT createSignedJWT(JWSAlgorithm jwsAlgorithm) { + JWSHeader jwsHeader = new JWSHeader.Builder(jwsAlgorithm).build(); + JWTClaimsSet claims = createJwtClaimsSet(); + + return new SignedJWT(jwsHeader, claims); + } + + private SignedJWT createSignedJWT(JWSAlgorithm jwsAlgorithm, JWTClaimsSet jwtClaimsSet) { + JWSHeader jwsHeader = new JWSHeader.Builder(jwsAlgorithm).build(); + + return new SignedJWT(jwsHeader, jwtClaimsSet); + } + + private JWTClaimsSet createJwtClaimsSet() { + return new JWTClaimsSet.Builder() + .issuer(CLIENT_ID) + .expirationTime(new Date()) + .audience("http://issuer.com/") + .build(); + } + +} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/config/TestJsonMessageSource.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/config/TestJsonMessageSource.java new file mode 100644 index 0000000000..04d7735d2f --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/config/TestJsonMessageSource.java @@ -0,0 +1,50 @@ +package org.mitre.openid.connect.config; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import java.text.MessageFormat; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@RunWith(MockitoJUnitRunner.class) +public class TestJsonMessageSource { + + @InjectMocks + private JsonMessageSource jsonMessageSource; + + @Spy + private ConfigurationPropertiesBean config; + + private Locale localeThatHasAFile = new Locale("en"); + + private Locale localeThatDoesNotHaveAFile = new Locale("xx"); + + @Before + public void setup() { + //test message files are located in test/resources/js/locale/ + Resource resource = new ClassPathResource("/resources/js/locale/"); + jsonMessageSource.setBaseDirectory(resource); + } + + @Test + public void verifyWhenLocaleExists_canResolveCode() { + MessageFormat mf = jsonMessageSource.resolveCode("testAttribute", localeThatHasAFile); + assertEquals(mf.getLocale().getLanguage(), "en"); + assertEquals(mf.toPattern(), "testValue"); + } + + @Test + public void verifyWhenLocaleDoesNotExist_cannotResolveCode() { + MessageFormat mf = jsonMessageSource.resolveCode("test", localeThatDoesNotHaveAFile); + assertNull(mf); + } +} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/empty.txt b/openid-connect-server/src/test/java/org/mitre/openid/connect/empty.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultApprovedSiteService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultApprovedSiteService.java index d413b0d531..8524145f0b 100644 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultApprovedSiteService.java +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultApprovedSiteService.java @@ -1,32 +1,34 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.repository.OAuth2TokenRepository; import org.mitre.openid.connect.model.ApprovedSite; import org.mitre.openid.connect.repository.ApprovedSiteRepository; import org.mitre.openid.connect.service.ApprovedSiteService; @@ -37,8 +39,12 @@ import org.mockito.runners.MockitoJUnitRunner; import org.springframework.test.annotation.Rollback; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + @RunWith(MockitoJUnitRunner.class) public class TestDefaultApprovedSiteService { @@ -52,6 +58,9 @@ public class TestDefaultApprovedSiteService { @Mock private ApprovedSiteRepository repository; + @Mock + private OAuth2TokenRepository tokenRepository; + @Mock private StatsService statsService; @@ -96,6 +105,8 @@ public void prepare() { public void clearApprovedSitesForClient_success() { Set setToReturn = Sets.newHashSet(site2, site3); Mockito.when(repository.getByClientId(client.getClientId())).thenReturn(setToReturn); + List tokens = ImmutableList.of(); + Mockito.when(tokenRepository.getAccessTokensForApprovedSite(any(ApprovedSite.class))).thenReturn(tokens); service.clearApprovedSitesForClient(client); diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultBlacklistedSiteService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultBlacklistedSiteService.java index 437a1b5689..79656cdddc 100644 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultBlacklistedSiteService.java +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultBlacklistedSiteService.java @@ -1,25 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.times; - import java.util.Set; import org.junit.Before; @@ -34,6 +31,11 @@ import com.google.common.collect.Sets; +import static org.mockito.Mockito.times; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + /** * @author wkim * diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultOIDCTokenService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultOIDCTokenService.java new file mode 100644 index 0000000000..01405474c0 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultOIDCTokenService.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import java.util.Date; + +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TestDefaultOIDCTokenService { + private static final String CLIENT_ID = "client"; + private static final String KEY_ID = "key"; + + private ConfigurationPropertiesBean configBean = new ConfigurationPropertiesBean(); + private ClientDetailsEntity client = new ClientDetailsEntity(); + private OAuth2AccessTokenEntity accessToken = new OAuth2AccessTokenEntity(); + private OAuth2Request request = new OAuth2Request(CLIENT_ID) { }; + + @Mock + private JWTSigningAndValidationService jwtService; + + @Before + public void prepare() { + configBean.setIssuer("https://auth.example.org/"); + + client.setClientId(CLIENT_ID); + Mockito.when(jwtService.getDefaultSigningAlgorithm()).thenReturn(JWSAlgorithm.RS256); + Mockito.when(jwtService.getDefaultSignerKeyId()).thenReturn(KEY_ID); + } + + @Test + public void invokesCustomClaimsHook() throws java.text.ParseException { + DefaultOIDCTokenService s = new DefaultOIDCTokenService() { + @Override + protected void addCustomIdTokenClaims(JWTClaimsSet.Builder idClaims, ClientDetailsEntity client, OAuth2Request request, + String sub, OAuth2AccessTokenEntity accessToken) { + idClaims.claim("test", "foo"); + } + }; + configure(s); + + JWT token = s.createIdToken(client, request, new Date(), "sub", accessToken); + Assert.assertEquals("foo", token.getJWTClaimsSet().getClaim("test")); + } + + + private void configure(DefaultOIDCTokenService s) { + s.setConfigBean(configBean); + s.setJwtService(jwtService); + } +} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultStatsService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultStatsService.java index 8be625b541..b5c1ae6b32 100644 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultStatsService.java +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultStatsService.java @@ -1,24 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - import java.util.HashSet; import java.util.Map; @@ -26,7 +24,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mitre.oauth2.model.ClientDetailsEntity; -import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.openid.connect.model.ApprovedSite; import org.mitre.openid.connect.service.ApprovedSiteService; import org.mockito.InjectMocks; @@ -36,6 +33,10 @@ import com.google.common.collect.Sets; +import static org.hamcrest.CoreMatchers.is; + +import static org.junit.Assert.assertThat; + /** * @author wkim * @@ -70,9 +71,6 @@ public class TestDefaultStatsService { @Mock private ApprovedSiteService approvedSiteService; - @Mock - private ClientDetailsEntityService clientService; - @InjectMocks private DefaultStatsService service = new DefaultStatsService(); @@ -83,7 +81,7 @@ public class TestDefaultStatsService { @Before public void prepare() { - Mockito.reset(approvedSiteService, clientService); + Mockito.reset(approvedSiteService); Mockito.when(ap1.getUserId()).thenReturn(userId1); Mockito.when(ap1.getClientId()).thenReturn(clientId1); @@ -110,11 +108,10 @@ public void prepare() { Mockito.when(client3.getId()).thenReturn(3L); Mockito.when(client4.getId()).thenReturn(4L); - Mockito.when(clientService.getAllClients()).thenReturn(Sets.newHashSet(client1, client2, client3, client4)); - Mockito.when(clientService.loadClientByClientId(clientId1)).thenReturn(client1); - Mockito.when(clientService.loadClientByClientId(clientId2)).thenReturn(client2); - Mockito.when(clientService.loadClientByClientId(clientId3)).thenReturn(client3); - Mockito.when(clientService.loadClientByClientId(clientId4)).thenReturn(client4); + Mockito.when(approvedSiteService.getByClientId(clientId1)).thenReturn(Sets.newHashSet(ap1, ap2)); + Mockito.when(approvedSiteService.getByClientId(clientId2)).thenReturn(Sets.newHashSet(ap3)); + Mockito.when(approvedSiteService.getByClientId(clientId3)).thenReturn(Sets.newHashSet(ap4)); + Mockito.when(approvedSiteService.getByClientId(clientId4)).thenReturn(Sets.newHashSet()); } @Test @@ -138,37 +135,13 @@ public void calculateSummaryStats() { assertThat(stats.get("clientCount"), is(3)); } - @Test - public void calculateByClientId_empty() { - - Mockito.when(approvedSiteService.getAll()).thenReturn(new HashSet()); - - Map stats = service.getByClientId(); - - assertThat(stats.get(1L), is(0)); - assertThat(stats.get(2L), is(0)); - assertThat(stats.get(3L), is(0)); - assertThat(stats.get(4L), is(0)); - } - - @Test - public void calculateByClientId() { - - Map stats = service.getByClientId(); - - assertThat(stats.get(1L), is(2)); - assertThat(stats.get(2L), is(1)); - assertThat(stats.get(3L), is(1)); - assertThat(stats.get(4L), is(0)); - } - @Test public void countForClientId() { - - assertThat(service.getCountForClientId(1L), is(2)); - assertThat(service.getCountForClientId(2L), is(1)); - assertThat(service.getCountForClientId(3L), is(1)); - assertThat(service.getCountForClientId(4L), is(0)); + // stats for ap1..ap4 + assertThat(service.getCountForClientId(clientId1).getApprovedSiteCount(), is(2)); + assertThat(service.getCountForClientId(clientId2).getApprovedSiteCount(), is(1)); + assertThat(service.getCountForClientId(clientId3).getApprovedSiteCount(), is(1)); + assertThat(service.getCountForClientId(clientId4).getApprovedSiteCount(), is(0)); } @Test diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultUserInfoService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultUserInfoService.java index f785a1fe05..e5e7070832 100644 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultUserInfoService.java +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultUserInfoService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +16,10 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.service.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,6 +38,9 @@ import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + /** * @author jricher * diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultUserInfoUserDetailsService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultUserInfoUserDetailsService.java deleted file mode 100644 index ec51728724..0000000000 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultUserInfoUserDetailsService.java +++ /dev/null @@ -1,121 +0,0 @@ -/******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * - * 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 org.mitre.openid.connect.service.impl; - -import static org.hamcrest.core.IsNot.not; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.matchers.JUnitMatchers.hasItem; - -import java.util.ArrayList; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mitre.openid.connect.model.DefaultUserInfo; -import org.mitre.openid.connect.model.UserInfo; -import org.mitre.openid.connect.repository.UserInfoRepository; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import com.google.common.collect.Lists; - -@RunWith(MockitoJUnitRunner.class) -public class TestDefaultUserInfoUserDetailsService { - - @InjectMocks - private DefaultUserInfoUserDetailsService service = new DefaultUserInfoUserDetailsService(); - - @Mock - private UserInfoRepository userInfoRepository; - - private UserInfo userInfoAdmin; - private UserInfo userInfoRegular; - - private String adminUsername = "username"; - private String regularUsername = "regular"; - private String adminSub = "adminSub12d3a1f34a2"; - private String regularSub = "regularSub652ha23b"; - - /** - * Initialize the service and the mocked repository. - * Initialize 2 users, one of them an admin, for use in unit tests. - */ - @Before - public void prepare() { - - - service.setAdmins(Lists.newArrayList(adminUsername)); - - userInfoAdmin = new DefaultUserInfo(); - userInfoAdmin.setPreferredUsername(adminUsername); - userInfoAdmin.setSub(adminSub); - - userInfoRegular = new DefaultUserInfo(); - userInfoRegular.setPreferredUsername(regularUsername); - userInfoRegular.setSub(regularSub); - - } - - /** - * Test loading an admin user, ensuring that the UserDetails object returned - * has both the ROLE_USER and ROLE_ADMIN authorities. - */ - @Test - public void loadByUsername_admin_success() { - - Mockito.when(userInfoRepository.getByUsername(adminUsername)).thenReturn(userInfoAdmin); - UserDetails user = service.loadUserByUsername(adminUsername); - ArrayList userAuthorities = Lists.newArrayList(user.getAuthorities()); - assertThat(userAuthorities, hasItem(DefaultUserInfoUserDetailsService.ROLE_ADMIN)); - assertThat(userAuthorities, hasItem(DefaultUserInfoUserDetailsService.ROLE_USER)); - assertEquals(user.getUsername(), adminSub); - - } - - /** - * Test loading a regular, non-admin user, ensuring that the returned UserDetails - * object has ROLE_USER but *not* ROLE_ADMIN. - */ - @Test - public void loadByUsername_regular_success() { - - Mockito.when(userInfoRepository.getByUsername(regularUsername)).thenReturn(userInfoRegular); - UserDetails user = service.loadUserByUsername(regularUsername); - ArrayList userAuthorities = Lists.newArrayList(user.getAuthorities()); - assertThat(userAuthorities, not(hasItem(DefaultUserInfoUserDetailsService.ROLE_ADMIN))); - assertThat(userAuthorities, hasItem(DefaultUserInfoUserDetailsService.ROLE_USER)); - assertEquals(user.getUsername(), regularSub); - - } - - /** - * If a user is not found, the loadByUsername method should throw an exception. - */ - @Test(expected = UsernameNotFoundException.class) - public void loadByUsername_nullUser() { - - Mockito.when(userInfoRepository.getByUsername(adminUsername)).thenReturn(null); - service.loadUserByUsername(adminUsername); - - } -} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultWhitelistedSiteService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultWhitelistedSiteService.java index d6a45ea693..40e8d65087 100644 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultWhitelistedSiteService.java +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestDefaultWhitelistedSiteService.java @@ -1,26 +1,22 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.service.impl; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,6 +27,12 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; + +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + /** * @author wkim * diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_0.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_0.java new file mode 100644 index 0000000000..74c275939f --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_0.java @@ -0,0 +1,970 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.isA; +import static org.mockito.Matchers.isNull; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.nimbusds.jwt.JWTParser; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import static org.junit.Assert.assertThat; + +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +public class TestMITREidDataService_1_0 { + + @Mock + private OAuth2ClientRepository clientRepository; + @Mock + private ApprovedSiteRepository approvedSiteRepository; + @Mock + private WhitelistedSiteRepository wlSiteRepository; + @Mock + private BlacklistedSiteRepository blSiteRepository; + @Mock + private AuthenticationHolderRepository authHolderRepository; + @Mock + private OAuth2TokenRepository tokenRepository; + @Mock + private SystemScopeRepository sysScopeRepository; + + @Captor + private ArgumentCaptor capturedRefreshTokens; + @Captor + private ArgumentCaptor capturedAccessTokens; + @Captor + private ArgumentCaptor capturedClients; + @Captor + private ArgumentCaptor capturedBlacklistedSites; + @Captor + private ArgumentCaptor capturedWhitelistedSites; + @Captor + private ArgumentCaptor capturedApprovedSites; + @Captor + private ArgumentCaptor capturedAuthHolders; + @Captor + private ArgumentCaptor capturedScope; + + @InjectMocks + private MITREidDataService_1_0 dataService; + + private DateFormatter formatter; + + @Before + public void prepare() { + formatter = new DateFormatter(); + formatter.setIso(ISO.DATE_TIME); + Mockito.reset(clientRepository, approvedSiteRepository, authHolderRepository, tokenRepository, sysScopeRepository, wlSiteRepository, blSiteRepository); + } + + private class refreshTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2RefreshTokenEntity entity1, OAuth2RefreshTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + @Test + public void testImportRefreshTokens() throws IOException, ParseException { + Date expirationDate1 = formatter.parse("2014-09-10T22:49:44.090+00:00", Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(mockedAuthHolder1); + + Date expirationDate2 = formatter.parse("2015-01-07T18:31:50.079+00:00", Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(mockedAuthHolder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + + System.err.println(configJson); + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 343L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 678L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder + verify(tokenRepository, times(6)).saveRefreshToken(capturedRefreshTokens.capture()); + + List savedRefreshTokens = new ArrayList(fakeDb.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.size(), is(2)); + + assertThat(savedRefreshTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedRefreshTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedRefreshTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedRefreshTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedRefreshTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedRefreshTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + private class accessTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2AccessTokenEntity entity1, OAuth2AccessTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + @Test + public void testImportAccessTokens() throws IOException, ParseException { + Date expirationDate1 = formatter.parse("2014-09-10T22:49:44.090+00:00", Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token1 = new OAuth2AccessTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0")); + token1.setAuthenticationHolder(mockedAuthHolder1); + token1.setScope(ImmutableSet.of("id-token")); + token1.setTokenType("Bearer"); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity mockRefreshToken2 = mock(OAuth2RefreshTokenEntity.class); + when(mockRefreshToken2.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token2 = new OAuth2AccessTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ")); + token2.setAuthenticationHolder(mockedAuthHolder2); + token2.setRefreshToken(mockRefreshToken2); + token2.setScope(ImmutableSet.of("openid", "offline_access", "email", "profile")); + token2.setTokenType("Bearer"); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"refreshTokenId\":null,\"idTokenId\":null,\"scope\":[\"id-token\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"refreshTokenId\":1,\"idTokenId\":1,\"scope\":[\"openid\",\"offline_access\",\"email\",\"profile\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ\"}" + + + " ]" + + "}"; + + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveAccessToken(isA(OAuth2AccessTokenEntity.class))).thenAnswer(new Answer() { + Long id = 343L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = (OAuth2AccessTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getAccessTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 234L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder, 1 times to update refresh token + verify(tokenRepository, times(7)).saveAccessToken(capturedAccessTokens.capture()); + + List savedAccessTokens = new ArrayList(fakeDb.values()); //capturedAccessTokens.getAllValues(); + Collections.sort(savedAccessTokens, new accessTokenIdComparator()); + + assertThat(savedAccessTokens.size(), is(2)); + + assertThat(savedAccessTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedAccessTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedAccessTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedAccessTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedAccessTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedAccessTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + + //several new client fields added in 1.1, perhaps additional tests for these should be added + @Test + public void testImportClients() throws IOException { + ClientDetailsEntity client1 = new ClientDetailsEntity(); + client1.setId(1L); + client1.setAccessTokenValiditySeconds(3600); + client1.setClientId("client1"); + client1.setClientSecret("clientsecret1"); + client1.setRedirectUris(ImmutableSet.of("http://foo.com/")); + client1.setScope(ImmutableSet.of("foo", "bar", "baz", "dolphin")); + client1.setGrantTypes(ImmutableSet.of("implicit", "authorization_code", "urn:ietf:params:oauth:grant_type:redelegate", "refresh_token")); + client1.setAllowIntrospection(true); + + ClientDetailsEntity client2 = new ClientDetailsEntity(); + client2.setId(2L); + client2.setAccessTokenValiditySeconds(3600); + client2.setClientId("client2"); + client2.setClientSecret("clientsecret2"); + client2.setRedirectUris(ImmutableSet.of("http://bar.baz.com/")); + client2.setScope(ImmutableSet.of("foo", "dolphin", "electric-wombat")); + client2.setGrantTypes(ImmutableSet.of("client_credentials", "urn:ietf:params:oauth:grant_type:redelegate")); + client2.setAllowIntrospection(false); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [" + + + "{\"id\":1,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client1\",\"secret\":\"clientsecret1\"," + + "\"redirectUris\":[\"http://foo.com/\"]," + + "\"scope\":[\"foo\",\"bar\",\"baz\",\"dolphin\"]," + + "\"grantTypes\":[\"implicit\",\"authorization_code\",\"urn:ietf:params:oauth:grant_type:redelegate\",\"refresh_token\"]," + + "\"allowIntrospection\":true}," + + "{\"id\":2,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client2\",\"secret\":\"clientsecret2\"," + + "\"redirectUris\":[\"http://bar.baz.com/\"]," + + "\"scope\":[\"foo\",\"dolphin\",\"electric-wombat\"]," + + "\"grantTypes\":[\"client_credentials\",\"urn:ietf:params:oauth:grant_type:redelegate\"]," + + "\"allowIntrospection\":false}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(clientRepository, times(2)).saveClient(capturedClients.capture()); + + List savedClients = capturedClients.getAllValues(); + + assertThat(savedClients.size(), is(2)); + + assertThat(savedClients.get(0).getAccessTokenValiditySeconds(), equalTo(client1.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(0).getClientId(), equalTo(client1.getClientId())); + assertThat(savedClients.get(0).getClientSecret(), equalTo(client1.getClientSecret())); + assertThat(savedClients.get(0).getRedirectUris(), equalTo(client1.getRedirectUris())); + assertThat(savedClients.get(0).getScope(), equalTo(client1.getScope())); + assertThat(savedClients.get(0).getGrantTypes(), equalTo(client1.getGrantTypes())); + assertThat(savedClients.get(0).isAllowIntrospection(), equalTo(client1.isAllowIntrospection())); + + assertThat(savedClients.get(1).getAccessTokenValiditySeconds(), equalTo(client2.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(1).getClientId(), equalTo(client2.getClientId())); + assertThat(savedClients.get(1).getClientSecret(), equalTo(client2.getClientSecret())); + assertThat(savedClients.get(1).getRedirectUris(), equalTo(client2.getRedirectUris())); + assertThat(savedClients.get(1).getScope(), equalTo(client2.getScope())); + assertThat(savedClients.get(1).getGrantTypes(), equalTo(client2.getGrantTypes())); + assertThat(savedClients.get(1).isAllowIntrospection(), equalTo(client2.isAllowIntrospection())); + } + + @Test + public void testImportBlacklistedSites() throws IOException { + BlacklistedSite site1 = new BlacklistedSite(); + site1.setId(1L); + site1.setUri("http://foo.com"); + + BlacklistedSite site2 = new BlacklistedSite(); + site2.setId(2L); + site2.setUri("http://bar.com"); + + BlacklistedSite site3 = new BlacklistedSite(); + site3.setId(3L); + site3.setUri("http://baz.com"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [" + + + "{\"id\":1,\"uri\":\"http://foo.com\"}," + + "{\"id\":2,\"uri\":\"http://bar.com\"}," + + "{\"id\":3,\"uri\":\"http://baz.com\"}" + + + " ]" + + "}"; + + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(blSiteRepository, times(3)).save(capturedBlacklistedSites.capture()); + + List savedSites = capturedBlacklistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getUri(), equalTo(site1.getUri())); + assertThat(savedSites.get(1).getUri(), equalTo(site2.getUri())); + assertThat(savedSites.get(2).getUri(), equalTo(site3.getUri())); + } + + @Test + public void testImportWhitelistedSites() throws IOException { + WhitelistedSite site1 = new WhitelistedSite(); + site1.setId(1L); + site1.setClientId("foo"); + + WhitelistedSite site2 = new WhitelistedSite(); + site2.setId(2L); + site2.setClientId("bar"); + + WhitelistedSite site3 = new WhitelistedSite(); + site3.setId(3L); + site3.setClientId("baz"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\"}," + + "{\"id\":2,\"clientId\":\"bar\"}," + + "{\"id\":3,\"clientId\":\"baz\"}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(wlSiteRepository.save(isA(WhitelistedSite.class))).thenAnswer(new Answer() { + Long id = 345L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = (WhitelistedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(wlSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + + dataService.importData(reader); + verify(wlSiteRepository, times(3)).save(capturedWhitelistedSites.capture()); + + List savedSites = capturedWhitelistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(2).getClientId(), equalTo(site3.getClientId())); + } + + @Test + public void testImportGrants() throws IOException, ParseException { + Date creationDate1 = formatter.parse("2014-09-10T22:49:44.090+00:00", Locale.ENGLISH); + Date accessDate1 = formatter.parse("2014-09-10T23:49:44.090+00:00", Locale.ENGLISH); + + OAuth2AccessTokenEntity mockToken1 = mock(OAuth2AccessTokenEntity.class); + when(mockToken1.getId()).thenReturn(1L); + + ApprovedSite site1 = new ApprovedSite(); + site1.setId(1L); + site1.setClientId("foo"); + site1.setCreationDate(creationDate1); + site1.setAccessDate(accessDate1); + site1.setUserId("user1"); + site1.setAllowedScopes(ImmutableSet.of("openid", "phone")); + when(mockToken1.getApprovedSite()).thenReturn(site1); + + Date creationDate2 = formatter.parse("2014-09-11T18:49:44.090+00:00", Locale.ENGLISH); + Date accessDate2 = formatter.parse("2014-09-11T20:49:44.090+00:00", Locale.ENGLISH); + Date timeoutDate2 = formatter.parse("2014-10-01T20:49:44.090+00:00", Locale.ENGLISH); + + ApprovedSite site2 = new ApprovedSite(); + site2.setId(2L); + site2.setClientId("bar"); + site2.setCreationDate(creationDate2); + site2.setAccessDate(accessDate2); + site2.setUserId("user2"); + site2.setAllowedScopes(ImmutableSet.of("openid", "offline_access", "email", "profile")); + site2.setTimeoutDate(timeoutDate2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\",\"creationDate\":\"2014-09-10T22:49:44.090+00:00\",\"accessDate\":\"2014-09-10T23:49:44.090+00:00\"," + + "\"userId\":\"user1\",\"whitelistedSiteId\":null,\"allowedScopes\":[\"openid\",\"phone\"], \"whitelistedSiteId\":1," + + "\"approvedAccessTokens\":[1]}," + + "{\"id\":2,\"clientId\":\"bar\",\"creationDate\":\"2014-09-11T18:49:44.090+00:00\",\"accessDate\":\"2014-09-11T20:49:44.090+00:00\"," + + "\"timeoutDate\":\"2014-10-01T20:49:44.090+00:00\",\"userId\":\"user2\"," + + "\"allowedScopes\":[\"openid\",\"offline_access\",\"email\",\"profile\"]}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(approvedSiteRepository.save(isA(ApprovedSite.class))).thenAnswer(new Answer() { + Long id = 343L; + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + ApprovedSite _site = (ApprovedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(approvedSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(wlSiteRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 244L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = mock(WhitelistedSite.class); + when(_site.getId()).thenReturn(id++); + return _site; + } + }); + when(tokenRepository.getAccessTokenById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 221L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = mock(OAuth2AccessTokenEntity.class); + when(_token.getId()).thenReturn(id++); + return _token; + } + }); + when(tokenRepository.getAccessTokensForApprovedSite(site1)).thenReturn(Lists.newArrayList(mockToken1)); + + dataService.importData(reader); + //2 for sites, 1 for updating access token ref on #1 + verify(approvedSiteRepository, times(3)).save(capturedApprovedSites.capture()); + + List savedSites = new ArrayList(fakeDb.values()); + + assertThat(savedSites.size(), is(2)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(0).getAccessDate(), equalTo(site1.getAccessDate())); + assertThat(savedSites.get(0).getCreationDate(), equalTo(site1.getCreationDate())); + assertThat(savedSites.get(0).getAllowedScopes(), equalTo(site1.getAllowedScopes())); + assertThat(savedSites.get(0).getTimeoutDate(), equalTo(site1.getTimeoutDate())); + + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(1).getAccessDate(), equalTo(site2.getAccessDate())); + assertThat(savedSites.get(1).getCreationDate(), equalTo(site2.getCreationDate())); + assertThat(savedSites.get(1).getAllowedScopes(), equalTo(site2.getAllowedScopes())); + assertThat(savedSites.get(1).getTimeoutDate(), equalTo(site2.getTimeoutDate())); + } + + @Test + public void testImportAuthenticationHolders() throws IOException { + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"}," + + "\"userAuthentication\":null}}," + + "{\"id\":2,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"}," + + "\"userAuthentication\":null}}" + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 356L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _holder = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_holder.getId() == null) { + _holder.setId(id++); + } + fakeDb.put(_holder.getId(), _holder); + return _holder; + } + }); + + dataService.importData(reader); + verify(authHolderRepository, times(2)).save(capturedAuthHolders.capture()); + + List savedAuthHolders = capturedAuthHolders.getAllValues(); + + assertThat(savedAuthHolders.size(), is(2)); + assertThat(savedAuthHolders.get(0).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder1.getAuthentication().getOAuth2Request().getClientId())); + assertThat(savedAuthHolders.get(1).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder2.getAuthentication().getOAuth2Request().getClientId())); + } + + @Test + public void testImportSystemScopes() throws IOException { + SystemScope scope1 = new SystemScope(); + scope1.setId(1L); + scope1.setValue("scope1"); + scope1.setDescription("Scope 1"); + scope1.setRestricted(true); + scope1.setDefaultScope(false); + scope1.setIcon("glass"); + + SystemScope scope2 = new SystemScope(); + scope2.setId(2L); + scope2.setValue("scope2"); + scope2.setDescription("Scope 2"); + scope2.setRestricted(false); + scope2.setDefaultScope(false); + scope2.setIcon("ball"); + + SystemScope scope3 = new SystemScope(); + scope3.setId(3L); + scope3.setValue("scope3"); + scope3.setDescription("Scope 3"); + scope3.setRestricted(false); + scope3.setDefaultScope(true); + scope3.setIcon("road"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [" + + + "{\"id\":1,\"description\":\"Scope 1\",\"icon\":\"glass\",\"value\":\"scope1\",\"allowDynReg\":false,\"defaultScope\":false}," + + "{\"id\":2,\"description\":\"Scope 2\",\"icon\":\"ball\",\"value\":\"scope2\",\"allowDynReg\":true,\"defaultScope\":false}," + + "{\"id\":3,\"description\":\"Scope 3\",\"icon\":\"road\",\"value\":\"scope3\",\"allowDynReg\":true,\"defaultScope\":true}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(sysScopeRepository, times(3)).save(capturedScope.capture()); + + List savedScopes = capturedScope.getAllValues(); + + assertThat(savedScopes.size(), is(3)); + assertThat(savedScopes.get(0).getValue(), equalTo(scope1.getValue())); + assertThat(savedScopes.get(0).getDescription(), equalTo(scope1.getDescription())); + assertThat(savedScopes.get(0).getIcon(), equalTo(scope1.getIcon())); + assertThat(savedScopes.get(0).isDefaultScope(), equalTo(scope1.isDefaultScope())); + assertThat(savedScopes.get(0).isRestricted(), equalTo(scope1.isRestricted())); + + assertThat(savedScopes.get(1).getValue(), equalTo(scope2.getValue())); + assertThat(savedScopes.get(1).getDescription(), equalTo(scope2.getDescription())); + assertThat(savedScopes.get(1).getIcon(), equalTo(scope2.getIcon())); + assertThat(savedScopes.get(1).isDefaultScope(), equalTo(scope2.isDefaultScope())); + assertThat(savedScopes.get(1).isRestricted(), equalTo(scope2.isRestricted())); + + assertThat(savedScopes.get(2).getValue(), equalTo(scope3.getValue())); + assertThat(savedScopes.get(2).getDescription(), equalTo(scope3.getDescription())); + assertThat(savedScopes.get(2).getIcon(), equalTo(scope3.getIcon())); + assertThat(savedScopes.get(2).isDefaultScope(), equalTo(scope3.isDefaultScope())); + assertThat(savedScopes.get(2).isRestricted(), equalTo(scope3.isRestricted())); + + } + + @Test + public void testFixRefreshTokenAuthHolderReferencesOnImport() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(holder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(holder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"}," + + "\"userAuthentication\":null}}," + + "{\"id\":2,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"}," + + "\"userAuthentication\":null}}" + + " ]," + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + final Map fakeRefreshTokenTable = new HashMap<>(); + final Map fakeAuthHolderTable = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 343L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeRefreshTokenTable.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeRefreshTokenTable.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 356L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _holder = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_holder.getId() == null) { + _holder.setId(id++); + } + fakeAuthHolderTable.put(_holder.getId(), _holder); + return _holder; + } + }); + when(authHolderRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeAuthHolderTable.get(_id); + } + }); + dataService.importData(reader); + + List savedRefreshTokens = new ArrayList(fakeRefreshTokenTable.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.get(0).getAuthenticationHolder().getId(), equalTo(356L)); + assertThat(savedRefreshTokens.get(1).getAuthenticationHolder().getId(), equalTo(357L)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testExportDisabled() throws IOException { + JsonWriter writer = new JsonWriter(new StringWriter()); + dataService.exportData(writer); + } + +} \ No newline at end of file diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_1.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_1.java new file mode 100644 index 0000000000..cfeb43a6f7 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_1.java @@ -0,0 +1,972 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.isA; +import static org.mockito.Matchers.isNull; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import com.google.common.collect.ImmutableSet; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.nimbusds.jwt.JWTParser; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import static org.junit.Assert.assertThat; + +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +public class TestMITREidDataService_1_1 { + + @Mock + private OAuth2ClientRepository clientRepository; + @Mock + private ApprovedSiteRepository approvedSiteRepository; + @Mock + private WhitelistedSiteRepository wlSiteRepository; + @Mock + private BlacklistedSiteRepository blSiteRepository; + @Mock + private AuthenticationHolderRepository authHolderRepository; + @Mock + private OAuth2TokenRepository tokenRepository; + @Mock + private SystemScopeRepository sysScopeRepository; + + @Captor + private ArgumentCaptor capturedRefreshTokens; + @Captor + private ArgumentCaptor capturedAccessTokens; + @Captor + private ArgumentCaptor capturedClients; + @Captor + private ArgumentCaptor capturedBlacklistedSites; + @Captor + private ArgumentCaptor capturedWhitelistedSites; + @Captor + private ArgumentCaptor capturedApprovedSites; + @Captor + private ArgumentCaptor capturedAuthHolders; + @Captor + private ArgumentCaptor capturedScope; + + @InjectMocks + private MITREidDataService_1_1 dataService; + private DateFormatter formatter; + + @Before + public void prepare() { + formatter = new DateFormatter(); + formatter.setIso(ISO.DATE_TIME); + + Mockito.reset(clientRepository, approvedSiteRepository, authHolderRepository, tokenRepository, sysScopeRepository, wlSiteRepository, blSiteRepository); + } + + + private class refreshTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2RefreshTokenEntity entity1, OAuth2RefreshTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + + @Test + public void testImportRefreshTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(mockedAuthHolder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(mockedAuthHolder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + + System.err.println(configJson); + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 332L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 131L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder + verify(tokenRepository, times(6)).saveRefreshToken(capturedRefreshTokens.capture()); + + List savedRefreshTokens = new ArrayList(fakeDb.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.size(), is(2)); + + assertThat(savedRefreshTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedRefreshTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedRefreshTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedRefreshTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedRefreshTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedRefreshTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + private class accessTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2AccessTokenEntity entity1, OAuth2AccessTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + @Test + public void testImportAccessTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token1 = new OAuth2AccessTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0")); + token1.setAuthenticationHolder(mockedAuthHolder1); + token1.setScope(ImmutableSet.of("id-token")); + token1.setTokenType("Bearer"); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity mockRefreshToken2 = mock(OAuth2RefreshTokenEntity.class); + when(mockRefreshToken2.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token2 = new OAuth2AccessTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ")); + token2.setAuthenticationHolder(mockedAuthHolder2); + token2.setRefreshToken(mockRefreshToken2); + token2.setScope(ImmutableSet.of("openid", "offline_access", "email", "profile")); + token2.setTokenType("Bearer"); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"refreshTokenId\":null,\"idTokenId\":null,\"scope\":[\"id-token\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"refreshTokenId\":1,\"idTokenId\":1,\"scope\":[\"openid\",\"offline_access\",\"email\",\"profile\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ\"}" + + + " ]" + + "}"; + + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveAccessToken(isA(OAuth2AccessTokenEntity.class))).thenAnswer(new Answer() { + Long id = 324L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = (OAuth2AccessTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getAccessTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 133L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder, 1 times to update refresh token + verify(tokenRepository, times(7)).saveAccessToken(capturedAccessTokens.capture()); + + List savedAccessTokens = new ArrayList(fakeDb.values()); //capturedAccessTokens.getAllValues(); + Collections.sort(savedAccessTokens, new accessTokenIdComparator()); + + assertThat(savedAccessTokens.size(), is(2)); + + assertThat(savedAccessTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedAccessTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedAccessTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedAccessTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedAccessTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedAccessTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + @Test + public void testImportClients() throws IOException { + ClientDetailsEntity client1 = new ClientDetailsEntity(); + client1.setId(1L); + client1.setAccessTokenValiditySeconds(3600); + client1.setClientId("client1"); + client1.setClientSecret("clientsecret1"); + client1.setRedirectUris(ImmutableSet.of("http://foo.com/")); + client1.setScope(ImmutableSet.of("foo", "bar", "baz", "dolphin")); + client1.setGrantTypes(ImmutableSet.of("implicit", "authorization_code", "urn:ietf:params:oauth:grant_type:redelegate", "refresh_token")); + client1.setAllowIntrospection(true); + + ClientDetailsEntity client2 = new ClientDetailsEntity(); + client2.setId(2L); + client2.setAccessTokenValiditySeconds(3600); + client2.setClientId("client2"); + client2.setClientSecret("clientsecret2"); + client2.setRedirectUris(ImmutableSet.of("http://bar.baz.com/")); + client2.setScope(ImmutableSet.of("foo", "dolphin", "electric-wombat")); + client2.setGrantTypes(ImmutableSet.of("client_credentials", "urn:ietf:params:oauth:grant_type:redelegate")); + client2.setAllowIntrospection(false); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [" + + + "{\"id\":1,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client1\",\"secret\":\"clientsecret1\"," + + "\"redirectUris\":[\"http://foo.com/\"]," + + "\"scope\":[\"foo\",\"bar\",\"baz\",\"dolphin\"]," + + "\"grantTypes\":[\"implicit\",\"authorization_code\",\"urn:ietf:params:oauth:grant_type:redelegate\",\"refresh_token\"]," + + "\"allowIntrospection\":true}," + + "{\"id\":2,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client2\",\"secret\":\"clientsecret2\"," + + "\"redirectUris\":[\"http://bar.baz.com/\"]," + + "\"scope\":[\"foo\",\"dolphin\",\"electric-wombat\"]," + + "\"grantTypes\":[\"client_credentials\",\"urn:ietf:params:oauth:grant_type:redelegate\"]," + + "\"allowIntrospection\":false}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(clientRepository, times(2)).saveClient(capturedClients.capture()); + + List savedClients = capturedClients.getAllValues(); + + assertThat(savedClients.size(), is(2)); + + assertThat(savedClients.get(0).getAccessTokenValiditySeconds(), equalTo(client1.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(0).getClientId(), equalTo(client1.getClientId())); + assertThat(savedClients.get(0).getClientSecret(), equalTo(client1.getClientSecret())); + assertThat(savedClients.get(0).getRedirectUris(), equalTo(client1.getRedirectUris())); + assertThat(savedClients.get(0).getScope(), equalTo(client1.getScope())); + assertThat(savedClients.get(0).getGrantTypes(), equalTo(client1.getGrantTypes())); + assertThat(savedClients.get(0).isAllowIntrospection(), equalTo(client1.isAllowIntrospection())); + + assertThat(savedClients.get(1).getAccessTokenValiditySeconds(), equalTo(client2.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(1).getClientId(), equalTo(client2.getClientId())); + assertThat(savedClients.get(1).getClientSecret(), equalTo(client2.getClientSecret())); + assertThat(savedClients.get(1).getRedirectUris(), equalTo(client2.getRedirectUris())); + assertThat(savedClients.get(1).getScope(), equalTo(client2.getScope())); + assertThat(savedClients.get(1).getGrantTypes(), equalTo(client2.getGrantTypes())); + assertThat(savedClients.get(1).isAllowIntrospection(), equalTo(client2.isAllowIntrospection())); + } + + @Test + public void testImportBlacklistedSites() throws IOException { + BlacklistedSite site1 = new BlacklistedSite(); + site1.setId(1L); + site1.setUri("http://foo.com"); + + BlacklistedSite site2 = new BlacklistedSite(); + site2.setId(2L); + site2.setUri("http://bar.com"); + + BlacklistedSite site3 = new BlacklistedSite(); + site3.setId(3L); + site3.setUri("http://baz.com"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [" + + + "{\"id\":1,\"uri\":\"http://foo.com\"}," + + "{\"id\":2,\"uri\":\"http://bar.com\"}," + + "{\"id\":3,\"uri\":\"http://baz.com\"}" + + + " ]" + + "}"; + + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(blSiteRepository, times(3)).save(capturedBlacklistedSites.capture()); + + List savedSites = capturedBlacklistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getUri(), equalTo(site1.getUri())); + assertThat(savedSites.get(1).getUri(), equalTo(site2.getUri())); + assertThat(savedSites.get(2).getUri(), equalTo(site3.getUri())); + } + + @Test + public void testImportWhitelistedSites() throws IOException { + WhitelistedSite site1 = new WhitelistedSite(); + site1.setId(1L); + site1.setClientId("foo"); + + WhitelistedSite site2 = new WhitelistedSite(); + site2.setId(2L); + site2.setClientId("bar"); + + WhitelistedSite site3 = new WhitelistedSite(); + site3.setId(3L); + site3.setClientId("baz"); + //site3.setAllowedScopes(null); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\"}," + + "{\"id\":2,\"clientId\":\"bar\"}," + + "{\"id\":3,\"clientId\":\"baz\"}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(wlSiteRepository.save(isA(WhitelistedSite.class))).thenAnswer(new Answer() { + Long id = 333L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = (WhitelistedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(wlSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + + dataService.importData(reader); + verify(wlSiteRepository, times(3)).save(capturedWhitelistedSites.capture()); + + List savedSites = capturedWhitelistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(2).getClientId(), equalTo(site3.getClientId())); + } + + @Test + public void testImportGrants() throws IOException, ParseException { + Date creationDate1 = formatter.parse("2014-09-10T22:49:44.090+00:00", Locale.ENGLISH); + Date accessDate1 = formatter.parse("2014-09-10T23:49:44.090+00:00", Locale.ENGLISH); + + OAuth2AccessTokenEntity mockToken1 = mock(OAuth2AccessTokenEntity.class); + when(mockToken1.getId()).thenReturn(1L); + + ApprovedSite site1 = new ApprovedSite(); + site1.setId(1L); + site1.setClientId("foo"); + site1.setCreationDate(creationDate1); + site1.setAccessDate(accessDate1); + site1.setUserId("user1"); + site1.setAllowedScopes(ImmutableSet.of("openid", "phone")); + when(mockToken1.getApprovedSite()).thenReturn(site1); + + Date creationDate2 = formatter.parse("2014-09-11T18:49:44.090+00:00", Locale.ENGLISH); + Date accessDate2 = formatter.parse("2014-09-11T20:49:44.090+00:00", Locale.ENGLISH); + Date timeoutDate2 = formatter.parse("2014-10-01T20:49:44.090+00:00", Locale.ENGLISH); + + ApprovedSite site2 = new ApprovedSite(); + site2.setId(2L); + site2.setClientId("bar"); + site2.setCreationDate(creationDate2); + site2.setAccessDate(accessDate2); + site2.setUserId("user2"); + site2.setAllowedScopes(ImmutableSet.of("openid", "offline_access", "email", "profile")); + site2.setTimeoutDate(timeoutDate2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\",\"creationDate\":\"2014-09-10T22:49:44.090+00:00\",\"accessDate\":\"2014-09-10T23:49:44.090+00:00\"," + + "\"userId\":\"user1\",\"whitelistedSiteId\":null,\"allowedScopes\":[\"openid\",\"phone\"], \"whitelistedSiteId\":1," + + "\"approvedAccessTokens\":[1]}," + + "{\"id\":2,\"clientId\":\"bar\",\"creationDate\":\"2014-09-11T18:49:44.090+00:00\",\"accessDate\":\"2014-09-11T20:49:44.090+00:00\"," + + "\"timeoutDate\":\"2014-10-01T20:49:44.090+00:00\",\"userId\":\"user2\"," + + "\"allowedScopes\":[\"openid\",\"offline_access\",\"email\",\"profile\"]}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(approvedSiteRepository.save(isA(ApprovedSite.class))).thenAnswer(new Answer() { + Long id = 364L; + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + ApprovedSite _site = (ApprovedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(approvedSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(wlSiteRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 432L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = mock(WhitelistedSite.class); + when(_site.getId()).thenReturn(id++); + return _site; + } + }); + when(tokenRepository.getAccessTokenById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 245L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = mock(OAuth2AccessTokenEntity.class); + when(_token.getId()).thenReturn(id++); + return _token; + } + }); + + dataService.importData(reader); + //2 for sites, 1 for updating access token ref on #1 + verify(approvedSiteRepository, times(3)).save(capturedApprovedSites.capture()); + + List savedSites = new ArrayList(fakeDb.values()); + + assertThat(savedSites.size(), is(2)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(0).getAccessDate(), equalTo(site1.getAccessDate())); + assertThat(savedSites.get(0).getCreationDate(), equalTo(site1.getCreationDate())); + assertThat(savedSites.get(0).getAllowedScopes(), equalTo(site1.getAllowedScopes())); + assertThat(savedSites.get(0).getTimeoutDate(), equalTo(site1.getTimeoutDate())); + + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(1).getAccessDate(), equalTo(site2.getAccessDate())); + assertThat(savedSites.get(1).getCreationDate(), equalTo(site2.getCreationDate())); + assertThat(savedSites.get(1).getAllowedScopes(), equalTo(site2.getAllowedScopes())); + assertThat(savedSites.get(1).getTimeoutDate(), equalTo(site2.getTimeoutDate())); + } + + @Test + public void testImportAuthenticationHolders() throws IOException { + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"}," + + "\"userAuthentication\":null}}," + + "{\"id\":2,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"}," + + "\"userAuthentication\":null}}" + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 243L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _site = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + + dataService.importData(reader); + verify(authHolderRepository, times(2)).save(capturedAuthHolders.capture()); + + List savedAuthHolders = capturedAuthHolders.getAllValues(); + + assertThat(savedAuthHolders.size(), is(2)); + assertThat(savedAuthHolders.get(0).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder1.getAuthentication().getOAuth2Request().getClientId())); + assertThat(savedAuthHolders.get(1).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder2.getAuthentication().getOAuth2Request().getClientId())); + } + + @Test + public void testImportSystemScopes() throws IOException { + SystemScope scope1 = new SystemScope(); + scope1.setId(1L); + scope1.setValue("scope1"); + scope1.setDescription("Scope 1"); + scope1.setRestricted(true); + scope1.setDefaultScope(false); + scope1.setIcon("glass"); + + SystemScope scope2 = new SystemScope(); + scope2.setId(2L); + scope2.setValue("scope2"); + scope2.setDescription("Scope 2"); + scope2.setRestricted(false); + scope2.setDefaultScope(false); + scope2.setIcon("ball"); + + SystemScope scope3 = new SystemScope(); + scope3.setId(3L); + scope3.setValue("scope3"); + scope3.setDescription("Scope 3"); + scope3.setRestricted(false); + scope3.setDefaultScope(true); + scope3.setIcon("road"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [" + + + "{\"id\":1,\"description\":\"Scope 1\",\"icon\":\"glass\",\"value\":\"scope1\",\"allowDynReg\":false,\"defaultScope\":false}," + + "{\"id\":2,\"description\":\"Scope 2\",\"icon\":\"ball\",\"value\":\"scope2\",\"allowDynReg\":true,\"defaultScope\":false}," + + "{\"id\":3,\"description\":\"Scope 3\",\"icon\":\"road\",\"value\":\"scope3\",\"allowDynReg\":true,\"defaultScope\":true}" + + + " ]" + + "}"; + + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(sysScopeRepository, times(3)).save(capturedScope.capture()); + + List savedScopes = capturedScope.getAllValues(); + + assertThat(savedScopes.size(), is(3)); + assertThat(savedScopes.get(0).getValue(), equalTo(scope1.getValue())); + assertThat(savedScopes.get(0).getDescription(), equalTo(scope1.getDescription())); + assertThat(savedScopes.get(0).getIcon(), equalTo(scope1.getIcon())); + assertThat(savedScopes.get(0).isDefaultScope(), equalTo(scope1.isDefaultScope())); + assertThat(savedScopes.get(0).isRestricted(), equalTo(scope1.isRestricted())); + + assertThat(savedScopes.get(1).getValue(), equalTo(scope2.getValue())); + assertThat(savedScopes.get(1).getDescription(), equalTo(scope2.getDescription())); + assertThat(savedScopes.get(1).getIcon(), equalTo(scope2.getIcon())); + assertThat(savedScopes.get(1).isDefaultScope(), equalTo(scope2.isDefaultScope())); + assertThat(savedScopes.get(1).isRestricted(), equalTo(scope2.isRestricted())); + + assertThat(savedScopes.get(2).getValue(), equalTo(scope3.getValue())); + assertThat(savedScopes.get(2).getDescription(), equalTo(scope3.getDescription())); + assertThat(savedScopes.get(2).getIcon(), equalTo(scope3.getIcon())); + assertThat(savedScopes.get(2).isDefaultScope(), equalTo(scope3.isDefaultScope())); + assertThat(savedScopes.get(2).isRestricted(), equalTo(scope3.isRestricted())); + + } + + @Test + public void testFixRefreshTokenAuthHolderReferencesOnImport() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(holder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(holder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"}," + + "\"userAuthentication\":null}}," + + "{\"id\":2,\"authentication\":{\"clientAuthorization\":{\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"}," + + "\"userAuthentication\":null}}" + + " ]," + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + System.err.println(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + final Map fakeRefreshTokenTable = new HashMap<>(); + final Map fakeAuthHolderTable = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 343L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeRefreshTokenTable.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeRefreshTokenTable.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 356L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _holder = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_holder.getId() == null) { + _holder.setId(id++); + } + fakeAuthHolderTable.put(_holder.getId(), _holder); + return _holder; + } + }); + when(authHolderRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeAuthHolderTable.get(_id); + } + }); + dataService.importData(reader); + + List savedRefreshTokens = new ArrayList(fakeRefreshTokenTable.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.get(0).getAuthenticationHolder().getId(), equalTo(356L)); + assertThat(savedRefreshTokens.get(1).getAuthenticationHolder().getId(), equalTo(357L)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testExportDisabled() throws IOException { + JsonWriter writer = new JsonWriter(new StringWriter()); + dataService.exportData(writer); + } + +} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_2.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_2.java new file mode 100644 index 0000000000..598471126f --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_2.java @@ -0,0 +1,976 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.isA; +import static org.mockito.Matchers.isNull; + +import java.io.IOException; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import com.google.common.collect.ImmutableSet; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.stream.JsonReader; +import com.nimbusds.jwt.JWTParser; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import static org.junit.Assert.assertThat; + +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +public class TestMITREidDataService_1_2 { + + private static Logger logger = LoggerFactory.getLogger(TestMITREidDataService_1_2.class); + + @Mock + private OAuth2ClientRepository clientRepository; + @Mock + private ApprovedSiteRepository approvedSiteRepository; + @Mock + private WhitelistedSiteRepository wlSiteRepository; + @Mock + private BlacklistedSiteRepository blSiteRepository; + @Mock + private AuthenticationHolderRepository authHolderRepository; + @Mock + private OAuth2TokenRepository tokenRepository; + @Mock + private SystemScopeRepository sysScopeRepository; + + @Captor + private ArgumentCaptor capturedRefreshTokens; + @Captor + private ArgumentCaptor capturedAccessTokens; + @Captor + private ArgumentCaptor capturedClients; + @Captor + private ArgumentCaptor capturedBlacklistedSites; + @Captor + private ArgumentCaptor capturedWhitelistedSites; + @Captor + private ArgumentCaptor capturedApprovedSites; + @Captor + private ArgumentCaptor capturedAuthHolders; + @Captor + private ArgumentCaptor capturedScope; + + @InjectMocks + private MITREidDataService_1_2 dataService; + private DateFormatter formatter; + + @Before + public void prepare() { + formatter = new DateFormatter(); + formatter.setIso(ISO.DATE_TIME); + + Mockito.reset(clientRepository, approvedSiteRepository, authHolderRepository, tokenRepository, sysScopeRepository, wlSiteRepository, blSiteRepository); + } + + private class refreshTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2RefreshTokenEntity entity1, OAuth2RefreshTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + + @Test + public void testImportRefreshTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(mockedAuthHolder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(mockedAuthHolder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + + logger.debug(configJson); + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 332L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 131L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder + verify(tokenRepository, times(6)).saveRefreshToken(capturedRefreshTokens.capture()); + + List savedRefreshTokens = new ArrayList(fakeDb.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.size(), is(2)); + + assertThat(savedRefreshTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedRefreshTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedRefreshTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedRefreshTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedRefreshTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedRefreshTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + private class accessTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2AccessTokenEntity entity1, OAuth2AccessTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + @Test + public void testImportAccessTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token1 = new OAuth2AccessTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0")); + token1.setAuthenticationHolder(mockedAuthHolder1); + token1.setScope(ImmutableSet.of("id-token")); + token1.setTokenType("Bearer"); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity mockRefreshToken2 = mock(OAuth2RefreshTokenEntity.class); + when(mockRefreshToken2.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token2 = new OAuth2AccessTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ")); + token2.setAuthenticationHolder(mockedAuthHolder2); + token2.setRefreshToken(mockRefreshToken2); + token2.setScope(ImmutableSet.of("openid", "offline_access", "email", "profile")); + token2.setTokenType("Bearer"); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"refreshTokenId\":null,\"idTokenId\":null,\"scope\":[\"id-token\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"refreshTokenId\":1,\"idTokenId\":1,\"scope\":[\"openid\",\"offline_access\",\"email\",\"profile\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ\"}" + + + " ]" + + "}"; + + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveAccessToken(isA(OAuth2AccessTokenEntity.class))).thenAnswer(new Answer() { + Long id = 324L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = (OAuth2AccessTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getAccessTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 133L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder, 1 times to update refresh token + verify(tokenRepository, times(7)).saveAccessToken(capturedAccessTokens.capture()); + + List savedAccessTokens = new ArrayList(fakeDb.values()); //capturedAccessTokens.getAllValues(); + Collections.sort(savedAccessTokens, new accessTokenIdComparator()); + + assertThat(savedAccessTokens.size(), is(2)); + + assertThat(savedAccessTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedAccessTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedAccessTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedAccessTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedAccessTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedAccessTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + @Test + public void testImportClients() throws IOException { + ClientDetailsEntity client1 = new ClientDetailsEntity(); + client1.setId(1L); + client1.setAccessTokenValiditySeconds(3600); + client1.setClientId("client1"); + client1.setClientSecret("clientsecret1"); + client1.setRedirectUris(ImmutableSet.of("http://foo.com/")); + client1.setScope(ImmutableSet.of("foo", "bar", "baz", "dolphin")); + client1.setGrantTypes(ImmutableSet.of("implicit", "authorization_code", "urn:ietf:params:oauth:grant_type:redelegate", "refresh_token")); + client1.setAllowIntrospection(true); + + ClientDetailsEntity client2 = new ClientDetailsEntity(); + client2.setId(2L); + client2.setAccessTokenValiditySeconds(3600); + client2.setClientId("client2"); + client2.setClientSecret("clientsecret2"); + client2.setRedirectUris(ImmutableSet.of("http://bar.baz.com/")); + client2.setScope(ImmutableSet.of("foo", "dolphin", "electric-wombat")); + client2.setGrantTypes(ImmutableSet.of("client_credentials", "urn:ietf:params:oauth:grant_type:redelegate")); + client2.setAllowIntrospection(false); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [" + + + "{\"id\":1,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client1\",\"secret\":\"clientsecret1\"," + + "\"redirectUris\":[\"http://foo.com/\"]," + + "\"scope\":[\"foo\",\"bar\",\"baz\",\"dolphin\"]," + + "\"grantTypes\":[\"implicit\",\"authorization_code\",\"urn:ietf:params:oauth:grant_type:redelegate\",\"refresh_token\"]," + + "\"allowIntrospection\":true}," + + "{\"id\":2,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client2\",\"secret\":\"clientsecret2\"," + + "\"redirectUris\":[\"http://bar.baz.com/\"]," + + "\"scope\":[\"foo\",\"dolphin\",\"electric-wombat\"]," + + "\"grantTypes\":[\"client_credentials\",\"urn:ietf:params:oauth:grant_type:redelegate\"]," + + "\"allowIntrospection\":false}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(clientRepository, times(2)).saveClient(capturedClients.capture()); + + List savedClients = capturedClients.getAllValues(); + + assertThat(savedClients.size(), is(2)); + + assertThat(savedClients.get(0).getAccessTokenValiditySeconds(), equalTo(client1.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(0).getClientId(), equalTo(client1.getClientId())); + assertThat(savedClients.get(0).getClientSecret(), equalTo(client1.getClientSecret())); + assertThat(savedClients.get(0).getRedirectUris(), equalTo(client1.getRedirectUris())); + assertThat(savedClients.get(0).getScope(), equalTo(client1.getScope())); + assertThat(savedClients.get(0).getGrantTypes(), equalTo(client1.getGrantTypes())); + assertThat(savedClients.get(0).isAllowIntrospection(), equalTo(client1.isAllowIntrospection())); + + assertThat(savedClients.get(1).getAccessTokenValiditySeconds(), equalTo(client2.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(1).getClientId(), equalTo(client2.getClientId())); + assertThat(savedClients.get(1).getClientSecret(), equalTo(client2.getClientSecret())); + assertThat(savedClients.get(1).getRedirectUris(), equalTo(client2.getRedirectUris())); + assertThat(savedClients.get(1).getScope(), equalTo(client2.getScope())); + assertThat(savedClients.get(1).getGrantTypes(), equalTo(client2.getGrantTypes())); + assertThat(savedClients.get(1).isAllowIntrospection(), equalTo(client2.isAllowIntrospection())); + } + + @Test + public void testImportBlacklistedSites() throws IOException { + BlacklistedSite site1 = new BlacklistedSite(); + site1.setId(1L); + site1.setUri("http://foo.com"); + + BlacklistedSite site2 = new BlacklistedSite(); + site2.setId(2L); + site2.setUri("http://bar.com"); + + BlacklistedSite site3 = new BlacklistedSite(); + site3.setId(3L); + site3.setUri("http://baz.com"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [" + + + "{\"id\":1,\"uri\":\"http://foo.com\"}," + + "{\"id\":2,\"uri\":\"http://bar.com\"}," + + "{\"id\":3,\"uri\":\"http://baz.com\"}" + + + " ]" + + "}"; + + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(blSiteRepository, times(3)).save(capturedBlacklistedSites.capture()); + + List savedSites = capturedBlacklistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getUri(), equalTo(site1.getUri())); + assertThat(savedSites.get(1).getUri(), equalTo(site2.getUri())); + assertThat(savedSites.get(2).getUri(), equalTo(site3.getUri())); + } + + @Test + public void testImportWhitelistedSites() throws IOException { + WhitelistedSite site1 = new WhitelistedSite(); + site1.setId(1L); + site1.setClientId("foo"); + + WhitelistedSite site2 = new WhitelistedSite(); + site2.setId(2L); + site2.setClientId("bar"); + + WhitelistedSite site3 = new WhitelistedSite(); + site3.setId(3L); + site3.setClientId("baz"); + //site3.setAllowedScopes(null); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\"}," + + "{\"id\":2,\"clientId\":\"bar\"}," + + "{\"id\":3,\"clientId\":\"baz\"}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(wlSiteRepository.save(isA(WhitelistedSite.class))).thenAnswer(new Answer() { + Long id = 333L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = (WhitelistedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(wlSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + + dataService.importData(reader); + verify(wlSiteRepository, times(3)).save(capturedWhitelistedSites.capture()); + + List savedSites = capturedWhitelistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(2).getClientId(), equalTo(site3.getClientId())); + } + + @Test + public void testImportGrants() throws IOException, ParseException { + Date creationDate1 = formatter.parse("2014-09-10T22:49:44.090+00:00", Locale.ENGLISH); + Date accessDate1 = formatter.parse("2014-09-10T23:49:44.090+00:00", Locale.ENGLISH); + + OAuth2AccessTokenEntity mockToken1 = mock(OAuth2AccessTokenEntity.class); + when(mockToken1.getId()).thenReturn(1L); + + ApprovedSite site1 = new ApprovedSite(); + site1.setId(1L); + site1.setClientId("foo"); + site1.setCreationDate(creationDate1); + site1.setAccessDate(accessDate1); + site1.setUserId("user1"); + site1.setAllowedScopes(ImmutableSet.of("openid", "phone")); + when(mockToken1.getApprovedSite()).thenReturn(site1); + + Date creationDate2 = formatter.parse("2014-09-11T18:49:44.090+00:00", Locale.ENGLISH); + Date accessDate2 = formatter.parse("2014-09-11T20:49:44.090+00:00", Locale.ENGLISH); + Date timeoutDate2 = formatter.parse("2014-10-01T20:49:44.090+00:00", Locale.ENGLISH); + + ApprovedSite site2 = new ApprovedSite(); + site2.setId(2L); + site2.setClientId("bar"); + site2.setCreationDate(creationDate2); + site2.setAccessDate(accessDate2); + site2.setUserId("user2"); + site2.setAllowedScopes(ImmutableSet.of("openid", "offline_access", "email", "profile")); + site2.setTimeoutDate(timeoutDate2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\",\"creationDate\":\"2014-09-10T22:49:44.090+00:00\",\"accessDate\":\"2014-09-10T23:49:44.090+00:00\"," + + "\"userId\":\"user1\",\"whitelistedSiteId\":null,\"allowedScopes\":[\"openid\",\"phone\"], \"whitelistedSiteId\":1," + + "\"approvedAccessTokens\":[1]}," + + "{\"id\":2,\"clientId\":\"bar\",\"creationDate\":\"2014-09-11T18:49:44.090+00:00\",\"accessDate\":\"2014-09-11T20:49:44.090+00:00\"," + + "\"timeoutDate\":\"2014-10-01T20:49:44.090+00:00\",\"userId\":\"user2\"," + + "\"allowedScopes\":[\"openid\",\"offline_access\",\"email\",\"profile\"]}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(approvedSiteRepository.save(isA(ApprovedSite.class))).thenAnswer(new Answer() { + Long id = 364L; + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + ApprovedSite _site = (ApprovedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(approvedSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(wlSiteRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 432L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = mock(WhitelistedSite.class); + when(_site.getId()).thenReturn(id++); + return _site; + } + }); + when(tokenRepository.getAccessTokenById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 245L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = mock(OAuth2AccessTokenEntity.class); + when(_token.getId()).thenReturn(id++); + return _token; + } + }); + + dataService.importData(reader); + //2 for sites, 1 for updating access token ref on #1 + verify(approvedSiteRepository, times(3)).save(capturedApprovedSites.capture()); + + List savedSites = new ArrayList(fakeDb.values()); + + assertThat(savedSites.size(), is(2)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(0).getAccessDate(), equalTo(site1.getAccessDate())); + assertThat(savedSites.get(0).getCreationDate(), equalTo(site1.getCreationDate())); + assertThat(savedSites.get(0).getAllowedScopes(), equalTo(site1.getAllowedScopes())); + assertThat(savedSites.get(0).getTimeoutDate(), equalTo(site1.getTimeoutDate())); + + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(1).getAccessDate(), equalTo(site2.getAccessDate())); + assertThat(savedSites.get(1).getCreationDate(), equalTo(site2.getCreationDate())); + assertThat(savedSites.get(1).getAllowedScopes(), equalTo(site2.getAllowedScopes())); + assertThat(savedSites.get(1).getTimeoutDate(), equalTo(site2.getTimeoutDate())); + } + + @Test + public void testImportAuthenticationHolders() throws IOException { + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"," + + "\"savedUserAuthentication\":null}," + + "{\"id\":2,\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"," + + "\"savedUserAuthentication\":null}" + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 243L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _site = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + + dataService.importData(reader); + verify(authHolderRepository, times(2)).save(capturedAuthHolders.capture()); + + List savedAuthHolders = capturedAuthHolders.getAllValues(); + + assertThat(savedAuthHolders.size(), is(2)); + assertThat(savedAuthHolders.get(0).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder1.getAuthentication().getOAuth2Request().getClientId())); + assertThat(savedAuthHolders.get(1).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder2.getAuthentication().getOAuth2Request().getClientId())); + } + + @Test + public void testImportSystemScopes() throws IOException { + SystemScope scope1 = new SystemScope(); + scope1.setId(1L); + scope1.setValue("scope1"); + scope1.setDescription("Scope 1"); + scope1.setRestricted(true); + scope1.setDefaultScope(false); + scope1.setIcon("glass"); + + SystemScope scope2 = new SystemScope(); + scope2.setId(2L); + scope2.setValue("scope2"); + scope2.setDescription("Scope 2"); + scope2.setRestricted(false); + scope2.setDefaultScope(false); + scope2.setIcon("ball"); + + SystemScope scope3 = new SystemScope(); + scope3.setId(3L); + scope3.setValue("scope3"); + scope3.setDescription("Scope 3"); + scope3.setRestricted(false); + scope3.setDefaultScope(true); + scope3.setIcon("road"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [" + + + "{\"id\":1,\"description\":\"Scope 1\",\"icon\":\"glass\",\"value\":\"scope1\",\"restricted\":true,\"defaultScope\":false}," + + "{\"id\":2,\"description\":\"Scope 2\",\"icon\":\"ball\",\"value\":\"scope2\",\"restricted\":false,\"defaultScope\":false}," + + "{\"id\":3,\"description\":\"Scope 3\",\"icon\":\"road\",\"value\":\"scope3\",\"restricted\":false,\"defaultScope\":true}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(sysScopeRepository, times(3)).save(capturedScope.capture()); + + List savedScopes = capturedScope.getAllValues(); + + assertThat(savedScopes.size(), is(3)); + assertThat(savedScopes.get(0).getValue(), equalTo(scope1.getValue())); + assertThat(savedScopes.get(0).getDescription(), equalTo(scope1.getDescription())); + assertThat(savedScopes.get(0).getIcon(), equalTo(scope1.getIcon())); + assertThat(savedScopes.get(0).isDefaultScope(), equalTo(scope1.isDefaultScope())); + assertThat(savedScopes.get(0).isRestricted(), equalTo(scope1.isRestricted())); + + assertThat(savedScopes.get(1).getValue(), equalTo(scope2.getValue())); + assertThat(savedScopes.get(1).getDescription(), equalTo(scope2.getDescription())); + assertThat(savedScopes.get(1).getIcon(), equalTo(scope2.getIcon())); + assertThat(savedScopes.get(1).isDefaultScope(), equalTo(scope2.isDefaultScope())); + assertThat(savedScopes.get(1).isRestricted(), equalTo(scope2.isRestricted())); + + assertThat(savedScopes.get(2).getValue(), equalTo(scope3.getValue())); + assertThat(savedScopes.get(2).getDescription(), equalTo(scope3.getDescription())); + assertThat(savedScopes.get(2).getIcon(), equalTo(scope3.getIcon())); + assertThat(savedScopes.get(2).isDefaultScope(), equalTo(scope3.isDefaultScope())); + assertThat(savedScopes.get(2).isRestricted(), equalTo(scope3.isRestricted())); + + } + + @Test + public void testFixRefreshTokenAuthHolderReferencesOnImport() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(holder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(holder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"authentication\":{\"authorizationRequest\":{\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"}," + + "\"userAuthentication\":null}}," + + "{\"id\":2,\"authentication\":{\"authorizationRequest\":{\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"}," + + "\"userAuthentication\":null}}" + + " ]," + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + final Map fakeRefreshTokenTable = new HashMap<>(); + final Map fakeAuthHolderTable = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 343L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeRefreshTokenTable.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeRefreshTokenTable.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 356L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _holder = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_holder.getId() == null) { + _holder.setId(id++); + } + fakeAuthHolderTable.put(_holder.getId(), _holder); + return _holder; + } + }); + when(authHolderRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeAuthHolderTable.get(_id); + } + }); + dataService.importData(reader); + + List savedRefreshTokens = new ArrayList(fakeRefreshTokenTable.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.get(0).getAuthenticationHolder().getId(), equalTo(356L)); + assertThat(savedRefreshTokens.get(1).getAuthenticationHolder().getId(), equalTo(357L)); + } + + private Set jsonArrayToStringSet(JsonArray a) { + Set s = new HashSet<>(); + for (JsonElement jsonElement : a) { + s.add(jsonElement.getAsString()); + } + return s; + } + +} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_3.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_3.java new file mode 100644 index 0000000000..19a12c2350 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestMITREidDataService_1_3.java @@ -0,0 +1,1858 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.openid.connect.service.impl; + +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.isA; +import static org.mockito.Matchers.isNull; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.PKCEAlgorithm; +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.repository.SystemScopeRepository; +import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.BlacklistedSite; +import org.mitre.openid.connect.model.WhitelistedSite; +import org.mitre.openid.connect.repository.ApprovedSiteRepository; +import org.mitre.openid.connect.repository.BlacklistedSiteRepository; +import org.mitre.openid.connect.repository.WhitelistedSiteRepository; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.nimbusds.jwt.JWTParser; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +public class TestMITREidDataService_1_3 { + + private static Logger logger = LoggerFactory.getLogger(TestMITREidDataService_1_3.class); + + @Mock + private OAuth2ClientRepository clientRepository; + @Mock + private ApprovedSiteRepository approvedSiteRepository; + @Mock + private WhitelistedSiteRepository wlSiteRepository; + @Mock + private BlacklistedSiteRepository blSiteRepository; + @Mock + private AuthenticationHolderRepository authHolderRepository; + @Mock + private OAuth2TokenRepository tokenRepository; + @Mock + private SystemScopeRepository sysScopeRepository; + + @Captor + private ArgumentCaptor capturedRefreshTokens; + @Captor + private ArgumentCaptor capturedAccessTokens; + @Captor + private ArgumentCaptor capturedClients; + @Captor + private ArgumentCaptor capturedBlacklistedSites; + @Captor + private ArgumentCaptor capturedWhitelistedSites; + @Captor + private ArgumentCaptor capturedApprovedSites; + @Captor + private ArgumentCaptor capturedAuthHolders; + @Captor + private ArgumentCaptor capturedScope; + + @InjectMocks + private MITREidDataService_1_3 dataService; + private DateFormatter formatter; + + @Before + public void prepare() { + formatter = new DateFormatter(); + formatter.setIso(ISO.DATE_TIME); + + Mockito.reset(clientRepository, approvedSiteRepository, authHolderRepository, tokenRepository, sysScopeRepository, wlSiteRepository, blSiteRepository); + } + + @Test + public void testExportRefreshTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(mockedAuthHolder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(mockedAuthHolder2); + + Set allRefreshTokens = ImmutableSet.of(token1, token2); + + Mockito.when(clientRepository.getAllClients()).thenReturn(new HashSet()); + Mockito.when(approvedSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(wlSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(blSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(authHolderRepository.getAll()).thenReturn(new ArrayList()); + Mockito.when(tokenRepository.getAllAccessTokens()).thenReturn(new HashSet()); + Mockito.when(tokenRepository.getAllRefreshTokens()).thenReturn(allRefreshTokens); + Mockito.when(sysScopeRepository.getAll()).thenReturn(new HashSet()); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + + // check our refresh token list (this test) + JsonArray refreshTokens = config.get(MITREidDataService.REFRESHTOKENS).getAsJsonArray(); + + assertThat(refreshTokens.size(), is(2)); + // check for both of our refresh tokens in turn + Set checked = new HashSet<>(); + for (JsonElement e : refreshTokens) { + assertThat(e.isJsonObject(), is(true)); + JsonObject token = e.getAsJsonObject(); + + OAuth2RefreshTokenEntity compare = null; + if (token.get("id").getAsLong() == token1.getId()) { + compare = token1; + } else if (token.get("id").getAsLong() == token2.getId()) { + compare = token2; + } + + if (compare == null) { + fail("Could not find matching id: " + token.get("id").getAsString()); + } else { + assertThat(token.get("id").getAsLong(), equalTo(compare.getId())); + assertThat(token.get("clientId").getAsString(), equalTo(compare.getClient().getClientId())); + assertThat(token.get("expiration").getAsString(), equalTo(formatter.print(compare.getExpiration(), Locale.ENGLISH))); + assertThat(token.get("value").getAsString(), equalTo(compare.getValue())); + assertThat(token.get("authenticationHolderId").getAsLong(), equalTo(compare.getAuthenticationHolder().getId())); + checked.add(compare); + } + } + // make sure all of our refresh tokens were found + assertThat(checked.containsAll(allRefreshTokens), is(true)); + } + + private class refreshTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2RefreshTokenEntity entity1, OAuth2RefreshTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + + @Test + public void testImportRefreshTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(mockedAuthHolder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(mockedAuthHolder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + + logger.debug(configJson); + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 332L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 131L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder + verify(tokenRepository, times(6)).saveRefreshToken(capturedRefreshTokens.capture()); + + List savedRefreshTokens = new ArrayList(fakeDb.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.size(), is(2)); + + assertThat(savedRefreshTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedRefreshTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedRefreshTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedRefreshTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedRefreshTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedRefreshTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + @Test + public void testExportAccessTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token1 = new OAuth2AccessTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0")); + token1.setAuthenticationHolder(mockedAuthHolder1); + token1.setScope(ImmutableSet.of("id-token")); + token1.setTokenType("Bearer"); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity mockRefreshToken2 = mock(OAuth2RefreshTokenEntity.class); + when(mockRefreshToken2.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token2 = new OAuth2AccessTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ")); + token2.setAuthenticationHolder(mockedAuthHolder2); + token2.setRefreshToken(mockRefreshToken2); + token2.setScope(ImmutableSet.of("openid", "offline_access", "email", "profile")); + token2.setTokenType("Bearer"); + + Set allAccessTokens = ImmutableSet.of(token1, token2); + + Mockito.when(clientRepository.getAllClients()).thenReturn(new HashSet()); + Mockito.when(approvedSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(wlSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(blSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(authHolderRepository.getAll()).thenReturn(new ArrayList()); + Mockito.when(tokenRepository.getAllRefreshTokens()).thenReturn(new HashSet()); + Mockito.when(tokenRepository.getAllAccessTokens()).thenReturn(allAccessTokens); + Mockito.when(sysScopeRepository.getAll()).thenReturn(new HashSet()); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + + // check our access token list (this test) + JsonArray accessTokens = config.get(MITREidDataService.ACCESSTOKENS).getAsJsonArray(); + + assertThat(accessTokens.size(), is(2)); + // check for both of our access tokens in turn + Set checked = new HashSet<>(); + for (JsonElement e : accessTokens) { + assertTrue(e.isJsonObject()); + JsonObject token = e.getAsJsonObject(); + + OAuth2AccessTokenEntity compare = null; + if (token.get("id").getAsLong() == token1.getId().longValue()) { + compare = token1; + } else if (token.get("id").getAsLong() == token2.getId().longValue()) { + compare = token2; + } + + if (compare == null) { + fail("Could not find matching id: " + token.get("id").getAsString()); + } else { + assertThat(token.get("id").getAsLong(), equalTo(compare.getId())); + assertThat(token.get("clientId").getAsString(), equalTo(compare.getClient().getClientId())); + assertThat(token.get("expiration").getAsString(), equalTo(formatter.print(compare.getExpiration(), Locale.ENGLISH))); + assertThat(token.get("value").getAsString(), equalTo(compare.getValue())); + assertThat(token.get("type").getAsString(), equalTo(compare.getTokenType())); + assertThat(token.get("authenticationHolderId").getAsLong(), equalTo(compare.getAuthenticationHolder().getId())); + assertTrue(token.get("scope").isJsonArray()); + assertThat(jsonArrayToStringSet(token.getAsJsonArray("scope")), equalTo(compare.getScope())); + if(token.get("refreshTokenId").isJsonNull()) { + assertNull(compare.getRefreshToken()); + } else { + assertThat(token.get("refreshTokenId").getAsLong(), equalTo(compare.getRefreshToken().getId())); + } + checked.add(compare); + } + } + // make sure all of our access tokens were found + assertThat(checked.containsAll(allAccessTokens), is(true)); + } + + private class accessTokenIdComparator implements Comparator { + @Override + public int compare(OAuth2AccessTokenEntity entity1, OAuth2AccessTokenEntity entity2) { + return entity1.getId().compareTo(entity2.getId()); + } + } + + @Test + public void testImportAccessTokens() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + AuthenticationHolderEntity mockedAuthHolder1 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder1.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token1 = new OAuth2AccessTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0")); + token1.setAuthenticationHolder(mockedAuthHolder1); + token1.setScope(ImmutableSet.of("id-token")); + token1.setTokenType("Bearer"); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + AuthenticationHolderEntity mockedAuthHolder2 = mock(AuthenticationHolderEntity.class); + when(mockedAuthHolder2.getId()).thenReturn(2L); + + OAuth2RefreshTokenEntity mockRefreshToken2 = mock(OAuth2RefreshTokenEntity.class); + when(mockRefreshToken2.getId()).thenReturn(1L); + + OAuth2AccessTokenEntity token2 = new OAuth2AccessTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ")); + token2.setAuthenticationHolder(mockedAuthHolder2); + token2.setRefreshToken(mockRefreshToken2); + token2.setScope(ImmutableSet.of("openid", "offline_access", "email", "profile")); + token2.setTokenType("Bearer"); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"refreshTokenId\":null,\"idTokenId\":null,\"scope\":[\"id-token\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3ODk5NjgsInN1YiI6IjkwMzQyLkFTREZKV0ZBIiwiYXRfaGFzaCI6InptTmt1QmNRSmNYQktNaVpFODZqY0EiLCJhdWQiOlsiY2xpZW50Il0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC9vcGVuaWQtY29ubmVjdC1zZXJ2ZXItd2ViYXBwXC8iLCJpYXQiOjE0MTI3ODkzNjh9.xkEJ9IMXpH7qybWXomfq9WOOlpGYnrvGPgey9UQ4GLzbQx7JC0XgJK83PmrmBZosvFPCmota7FzI_BtwoZLgAZfFiH6w3WIlxuogoH-TxmYbxEpTHoTsszZppkq9mNgOlArV4jrR9y3TPo4MovsH71dDhS_ck-CvAlJunHlqhs0\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"refreshTokenId\":1,\"idTokenId\":1,\"scope\":[\"openid\",\"offline_access\",\"email\",\"profile\"],\"type\":\"Bearer\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTI3OTI5NjgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwODBcL29wZW5pZC1jb25uZWN0LXNlcnZlci13ZWJhcHBcLyIsImp0aSI6IjBmZGE5ZmRiLTYyYzItNGIzZS05OTdiLWU0M2VhMDUwMzNiOSIsImlhdCI6MTQxMjc4OTM2OH0.xgaVpRLYE5MzbgXfE0tZt823tjAm6Oh3_kdR1P2I9jRLR6gnTlBQFlYi3Y_0pWNnZSerbAE8Tn6SJHZ9k-curVG0-ByKichV7CNvgsE5X_2wpEaUzejvKf8eZ-BammRY-ie6yxSkAarcUGMvGGOLbkFcz5CtrBpZhfd75J49BIQ\"}" + + + " ]" + + "}"; + + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(tokenRepository.saveAccessToken(isA(OAuth2AccessTokenEntity.class))).thenAnswer(new Answer() { + Long id = 324L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = (OAuth2AccessTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeDb.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getAccessTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 133L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _auth = mock(AuthenticationHolderEntity.class); + when(_auth.getId()).thenReturn(id); + id++; + return _auth; + } + }); + dataService.importData(reader); + //2 times for token, 2 times to update client, 2 times to update authHolder, 1 times to update refresh token + verify(tokenRepository, times(7)).saveAccessToken(capturedAccessTokens.capture()); + + List savedAccessTokens = new ArrayList(fakeDb.values()); //capturedAccessTokens.getAllValues(); + Collections.sort(savedAccessTokens, new accessTokenIdComparator()); + + assertThat(savedAccessTokens.size(), is(2)); + + assertThat(savedAccessTokens.get(0).getClient().getClientId(), equalTo(token1.getClient().getClientId())); + assertThat(savedAccessTokens.get(0).getExpiration(), equalTo(token1.getExpiration())); + assertThat(savedAccessTokens.get(0).getValue(), equalTo(token1.getValue())); + + assertThat(savedAccessTokens.get(1).getClient().getClientId(), equalTo(token2.getClient().getClientId())); + assertThat(savedAccessTokens.get(1).getExpiration(), equalTo(token2.getExpiration())); + assertThat(savedAccessTokens.get(1).getValue(), equalTo(token2.getValue())); + } + + @Test + public void testExportClients() throws IOException { + ClientDetailsEntity client1 = new ClientDetailsEntity(); + client1.setId(1L); + client1.setAccessTokenValiditySeconds(3600); + client1.setClientId("client1"); + client1.setClientSecret("clientsecret1"); + client1.setRedirectUris(ImmutableSet.of("http://foo.com/")); + client1.setScope(ImmutableSet.of("foo", "bar", "baz", "dolphin")); + client1.setGrantTypes(ImmutableSet.of("implicit", "authorization_code", "urn:ietf:params:oauth:grant_type:redelegate", "refresh_token")); + client1.setAllowIntrospection(true); + + ClientDetailsEntity client2 = new ClientDetailsEntity(); + client2.setId(2L); + client2.setAccessTokenValiditySeconds(3600); + client2.setClientId("client2"); + client2.setClientSecret("clientsecret2"); + client2.setRedirectUris(ImmutableSet.of("http://bar.baz.com/")); + client2.setScope(ImmutableSet.of("foo", "dolphin", "electric-wombat")); + client2.setGrantTypes(ImmutableSet.of("client_credentials", "urn:ietf:params:oauth:grant_type:redelegate")); + client2.setAllowIntrospection(false); + client2.setCodeChallengeMethod(PKCEAlgorithm.S256); + + Set allClients = ImmutableSet.of(client1, client2); + + Mockito.when(clientRepository.getAllClients()).thenReturn(allClients); + Mockito.when(approvedSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(wlSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(blSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(authHolderRepository.getAll()).thenReturn(new ArrayList()); + Mockito.when(tokenRepository.getAllAccessTokens()).thenReturn(new HashSet()); + Mockito.when(tokenRepository.getAllRefreshTokens()).thenReturn(new HashSet()); + Mockito.when(sysScopeRepository.getAll()).thenReturn(new HashSet()); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + + // check our client list (this test) + JsonArray clients = config.get(MITREidDataService.CLIENTS).getAsJsonArray(); + + assertThat(clients.size(), is(2)); + // check for both of our clients in turn + Set checked = new HashSet<>(); + for (JsonElement e : clients) { + assertThat(e.isJsonObject(), is(true)); + JsonObject client = e.getAsJsonObject(); + + ClientDetailsEntity compare = null; + if (client.get("clientId").getAsString().equals(client1.getClientId())) { + compare = client1; + } else if (client.get("clientId").getAsString().equals(client2.getClientId())) { + compare = client2; + } + + if (compare == null) { + fail("Could not find matching clientId: " + client.get("clientId").getAsString()); + } else { + assertThat(client.get("clientId").getAsString(), equalTo(compare.getClientId())); + assertThat(client.get("secret").getAsString(), equalTo(compare.getClientSecret())); + assertThat(client.get("accessTokenValiditySeconds").getAsInt(), equalTo(compare.getAccessTokenValiditySeconds())); + assertThat(client.get("allowIntrospection").getAsBoolean(), equalTo(compare.isAllowIntrospection())); + assertThat(jsonArrayToStringSet(client.get("redirectUris").getAsJsonArray()), equalTo(compare.getRedirectUris())); + assertThat(jsonArrayToStringSet(client.get("scope").getAsJsonArray()), equalTo(compare.getScope())); + assertThat(jsonArrayToStringSet(client.get("grantTypes").getAsJsonArray()), equalTo(compare.getGrantTypes())); + assertThat((client.has("codeChallengeMethod") && !client.get("codeChallengeMethod").isJsonNull()) ? PKCEAlgorithm.parse(client.get("codeChallengeMethod").getAsString()) : null, equalTo(compare.getCodeChallengeMethod())); + checked.add(compare); + } + } + // make sure all of our clients were found + assertThat(checked.containsAll(allClients), is(true)); + } + + @Test + public void testImportClients() throws IOException { + ClientDetailsEntity client1 = new ClientDetailsEntity(); + client1.setId(1L); + client1.setAccessTokenValiditySeconds(3600); + client1.setClientId("client1"); + client1.setClientSecret("clientsecret1"); + client1.setRedirectUris(ImmutableSet.of("http://foo.com/")); + client1.setScope(ImmutableSet.of("foo", "bar", "baz", "dolphin")); + client1.setGrantTypes(ImmutableSet.of("implicit", "authorization_code", "urn:ietf:params:oauth:grant_type:redelegate", "refresh_token")); + client1.setAllowIntrospection(true); + + ClientDetailsEntity client2 = new ClientDetailsEntity(); + client2.setId(2L); + client2.setAccessTokenValiditySeconds(3600); + client2.setClientId("client2"); + client2.setClientSecret("clientsecret2"); + client2.setRedirectUris(ImmutableSet.of("http://bar.baz.com/")); + client2.setScope(ImmutableSet.of("foo", "dolphin", "electric-wombat")); + client2.setGrantTypes(ImmutableSet.of("client_credentials", "urn:ietf:params:oauth:grant_type:redelegate")); + client2.setAllowIntrospection(false); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [" + + + "{\"id\":1,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client1\",\"secret\":\"clientsecret1\"," + + "\"redirectUris\":[\"http://foo.com/\"]," + + "\"scope\":[\"foo\",\"bar\",\"baz\",\"dolphin\"]," + + "\"grantTypes\":[\"implicit\",\"authorization_code\",\"urn:ietf:params:oauth:grant_type:redelegate\",\"refresh_token\"]," + + "\"allowIntrospection\":true}," + + "{\"id\":2,\"accessTokenValiditySeconds\":3600,\"clientId\":\"client2\",\"secret\":\"clientsecret2\"," + + "\"redirectUris\":[\"http://bar.baz.com/\"]," + + "\"scope\":[\"foo\",\"dolphin\",\"electric-wombat\"]," + + "\"grantTypes\":[\"client_credentials\",\"urn:ietf:params:oauth:grant_type:redelegate\"]," + + "\"allowIntrospection\":false}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(clientRepository, times(2)).saveClient(capturedClients.capture()); + + List savedClients = capturedClients.getAllValues(); + + assertThat(savedClients.size(), is(2)); + + assertThat(savedClients.get(0).getAccessTokenValiditySeconds(), equalTo(client1.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(0).getClientId(), equalTo(client1.getClientId())); + assertThat(savedClients.get(0).getClientSecret(), equalTo(client1.getClientSecret())); + assertThat(savedClients.get(0).getRedirectUris(), equalTo(client1.getRedirectUris())); + assertThat(savedClients.get(0).getScope(), equalTo(client1.getScope())); + assertThat(savedClients.get(0).getGrantTypes(), equalTo(client1.getGrantTypes())); + assertThat(savedClients.get(0).isAllowIntrospection(), equalTo(client1.isAllowIntrospection())); + + assertThat(savedClients.get(1).getAccessTokenValiditySeconds(), equalTo(client2.getAccessTokenValiditySeconds())); + assertThat(savedClients.get(1).getClientId(), equalTo(client2.getClientId())); + assertThat(savedClients.get(1).getClientSecret(), equalTo(client2.getClientSecret())); + assertThat(savedClients.get(1).getRedirectUris(), equalTo(client2.getRedirectUris())); + assertThat(savedClients.get(1).getScope(), equalTo(client2.getScope())); + assertThat(savedClients.get(1).getGrantTypes(), equalTo(client2.getGrantTypes())); + assertThat(savedClients.get(1).isAllowIntrospection(), equalTo(client2.isAllowIntrospection())); + } + + @Test + public void testExportBlacklistedSites() throws IOException { + BlacklistedSite site1 = new BlacklistedSite(); + site1.setId(1L); + site1.setUri("http://foo.com"); + + BlacklistedSite site2 = new BlacklistedSite(); + site2.setId(2L); + site2.setUri("http://bar.com"); + + BlacklistedSite site3 = new BlacklistedSite(); + site3.setId(3L); + site3.setUri("http://baz.com"); + + Set allBlacklistedSites = ImmutableSet.of(site1, site2, site3); + + Mockito.when(clientRepository.getAllClients()).thenReturn(new HashSet()); + Mockito.when(approvedSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(wlSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(blSiteRepository.getAll()).thenReturn(allBlacklistedSites); + Mockito.when(authHolderRepository.getAll()).thenReturn(new ArrayList()); + Mockito.when(tokenRepository.getAllAccessTokens()).thenReturn(new HashSet()); + Mockito.when(tokenRepository.getAllRefreshTokens()).thenReturn(new HashSet()); + Mockito.when(sysScopeRepository.getAll()).thenReturn(new HashSet()); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + // check our scope list (this test) + JsonArray sites = config.get(MITREidDataService.BLACKLISTEDSITES).getAsJsonArray(); + + assertThat(sites.size(), is(3)); + // check for both of our sites in turn + Set checked = new HashSet<>(); + for (JsonElement e : sites) { + assertThat(e.isJsonObject(), is(true)); + JsonObject site = e.getAsJsonObject(); + + BlacklistedSite compare = null; + if (site.get("id").getAsLong() == site1.getId().longValue()) { + compare = site1; + } else if (site.get("id").getAsLong() == site2.getId().longValue()) { + compare = site2; + } else if (site.get("id").getAsLong() == site3.getId().longValue()) { + compare = site3; + } + + if (compare == null) { + fail("Could not find matching blacklisted site id: " + site.get("id").getAsString()); + } else { + assertThat(site.get("uri").getAsString(), equalTo(compare.getUri())); + checked.add(compare); + } + } + // make sure all of our clients were found + assertThat(checked.containsAll(allBlacklistedSites), is(true)); + + } + + @Test + public void testImportBlacklistedSites() throws IOException { + BlacklistedSite site1 = new BlacklistedSite(); + site1.setId(1L); + site1.setUri("http://foo.com"); + + BlacklistedSite site2 = new BlacklistedSite(); + site2.setId(2L); + site2.setUri("http://bar.com"); + + BlacklistedSite site3 = new BlacklistedSite(); + site3.setId(3L); + site3.setUri("http://baz.com"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [" + + + "{\"id\":1,\"uri\":\"http://foo.com\"}," + + "{\"id\":2,\"uri\":\"http://bar.com\"}," + + "{\"id\":3,\"uri\":\"http://baz.com\"}" + + + " ]" + + "}"; + + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(blSiteRepository, times(3)).save(capturedBlacklistedSites.capture()); + + List savedSites = capturedBlacklistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getUri(), equalTo(site1.getUri())); + assertThat(savedSites.get(1).getUri(), equalTo(site2.getUri())); + assertThat(savedSites.get(2).getUri(), equalTo(site3.getUri())); + } + + @Test + public void testExportWhitelistedSites() throws IOException { + WhitelistedSite site1 = new WhitelistedSite(); + site1.setId(1L); + site1.setClientId("foo"); + + WhitelistedSite site2 = new WhitelistedSite(); + site2.setId(2L); + site2.setClientId("bar"); + + WhitelistedSite site3 = new WhitelistedSite(); + site3.setId(3L); + site3.setClientId("baz"); + + Set allWhitelistedSites = ImmutableSet.of(site1, site2, site3); + + Mockito.when(clientRepository.getAllClients()).thenReturn(new HashSet()); + Mockito.when(approvedSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(blSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(wlSiteRepository.getAll()).thenReturn(allWhitelistedSites); + Mockito.when(authHolderRepository.getAll()).thenReturn(new ArrayList()); + Mockito.when(tokenRepository.getAllAccessTokens()).thenReturn(new HashSet()); + Mockito.when(tokenRepository.getAllRefreshTokens()).thenReturn(new HashSet()); + Mockito.when(sysScopeRepository.getAll()).thenReturn(new HashSet()); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + // check our scope list (this test) + JsonArray sites = config.get(MITREidDataService.WHITELISTEDSITES).getAsJsonArray(); + + assertThat(sites.size(), is(3)); + // check for both of our sites in turn + Set checked = new HashSet<>(); + for (JsonElement e : sites) { + assertThat(e.isJsonObject(), is(true)); + JsonObject site = e.getAsJsonObject(); + + WhitelistedSite compare = null; + if (site.get("id").getAsLong() == site1.getId().longValue()) { + compare = site1; + } else if (site.get("id").getAsLong() == site2.getId().longValue()) { + compare = site2; + } else if (site.get("id").getAsLong() == site3.getId().longValue()) { + compare = site3; + } + + if (compare == null) { + fail("Could not find matching whitelisted site id: " + site.get("id").getAsString()); + } else { + assertThat(site.get("clientId").getAsString(), equalTo(compare.getClientId())); + checked.add(compare); + } + } + // make sure all of our clients were found + assertThat(checked.containsAll(allWhitelistedSites), is(true)); + + } + + @Test + public void testImportWhitelistedSites() throws IOException { + WhitelistedSite site1 = new WhitelistedSite(); + site1.setId(1L); + site1.setClientId("foo"); + + WhitelistedSite site2 = new WhitelistedSite(); + site2.setId(2L); + site2.setClientId("bar"); + + WhitelistedSite site3 = new WhitelistedSite(); + site3.setId(3L); + site3.setClientId("baz"); + //site3.setAllowedScopes(null); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\"}," + + "{\"id\":2,\"clientId\":\"bar\"}," + + "{\"id\":3,\"clientId\":\"baz\"}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(wlSiteRepository.save(isA(WhitelistedSite.class))).thenAnswer(new Answer() { + Long id = 333L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = (WhitelistedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(wlSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + + dataService.importData(reader); + verify(wlSiteRepository, times(3)).save(capturedWhitelistedSites.capture()); + + List savedSites = capturedWhitelistedSites.getAllValues(); + + assertThat(savedSites.size(), is(3)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(2).getClientId(), equalTo(site3.getClientId())); + } + + @Test + public void testExportGrants() throws IOException, ParseException { + Date creationDate1 = formatter.parse("2014-09-10T22:49:44.090+00:00", Locale.ENGLISH); + Date accessDate1 = formatter.parse("2014-09-10T23:49:44.090+00:00", Locale.ENGLISH); + + OAuth2AccessTokenEntity mockToken1 = mock(OAuth2AccessTokenEntity.class); + when(mockToken1.getId()).thenReturn(1L); + + ApprovedSite site1 = new ApprovedSite(); + site1.setId(1L); + site1.setClientId("foo"); + site1.setCreationDate(creationDate1); + site1.setAccessDate(accessDate1); + site1.setUserId("user1"); + site1.setAllowedScopes(ImmutableSet.of("openid", "phone")); + when(mockToken1.getApprovedSite()).thenReturn(site1); + + Date creationDate2 = formatter.parse("2014-09-11T18:49:44.090+00:00", Locale.ENGLISH); + Date accessDate2 = formatter.parse("2014-09-11T20:49:44.090+00:00", Locale.ENGLISH); + Date timeoutDate2 = formatter.parse("2014-10-01T20:49:44.090+00:00", Locale.ENGLISH); + + ApprovedSite site2 = new ApprovedSite(); + site2.setId(2L); + site2.setClientId("bar"); + site2.setCreationDate(creationDate2); + site2.setAccessDate(accessDate2); + site2.setUserId("user2"); + site2.setAllowedScopes(ImmutableSet.of("openid", "offline_access", "email", "profile")); + site2.setTimeoutDate(timeoutDate2); + + Set allApprovedSites = ImmutableSet.of(site1, site2); + + Mockito.when(clientRepository.getAllClients()).thenReturn(new HashSet()); + Mockito.when(approvedSiteRepository.getAll()).thenReturn(allApprovedSites); + Mockito.when(blSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(wlSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(authHolderRepository.getAll()).thenReturn(new ArrayList()); + Mockito.when(tokenRepository.getAllAccessTokens()).thenReturn(new HashSet()); + Mockito.when(tokenRepository.getAllRefreshTokens()).thenReturn(new HashSet()); + Mockito.when(sysScopeRepository.getAll()).thenReturn(new HashSet()); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + // check our scope list (this test) + JsonArray sites = config.get(MITREidDataService.GRANTS).getAsJsonArray(); + + assertThat(sites.size(), is(2)); + // check for both of our sites in turn + Set checked = new HashSet<>(); + for (JsonElement e : sites) { + assertThat(e.isJsonObject(), is(true)); + JsonObject site = e.getAsJsonObject(); + + ApprovedSite compare = null; + if (site.get("id").getAsLong() == site1.getId().longValue()) { + compare = site1; + } else if (site.get("id").getAsLong() == site2.getId().longValue()) { + compare = site2; + } + + if (compare == null) { + fail("Could not find matching whitelisted site id: " + site.get("id").getAsString()); + } else { + assertThat(site.get("clientId").getAsString(), equalTo(compare.getClientId())); + assertThat(site.get("creationDate").getAsString(), equalTo(formatter.print(compare.getCreationDate(), Locale.ENGLISH))); + assertThat(site.get("accessDate").getAsString(), equalTo(formatter.print(compare.getAccessDate(), Locale.ENGLISH))); + if(site.get("timeoutDate").isJsonNull()) { + assertNull(compare.getTimeoutDate()); + } else { + assertThat(site.get("timeoutDate").getAsString(), equalTo(formatter.print(compare.getTimeoutDate(), Locale.ENGLISH))); + } + assertThat(site.get("userId").getAsString(), equalTo(compare.getUserId())); + assertThat(jsonArrayToStringSet(site.getAsJsonArray("allowedScopes")), equalTo(compare.getAllowedScopes())); + checked.add(compare); + } + } + // make sure all of our clients were found + assertThat(checked.containsAll(allApprovedSites), is(true)); + } + + @Test + public void testImportGrants() throws IOException, ParseException { + Date creationDate1 = formatter.parse("2014-09-10T22:49:44.090+00:00", Locale.ENGLISH); + Date accessDate1 = formatter.parse("2014-09-10T23:49:44.090+00:00", Locale.ENGLISH); + + OAuth2AccessTokenEntity mockToken1 = mock(OAuth2AccessTokenEntity.class); + when(mockToken1.getId()).thenReturn(1L); + + ApprovedSite site1 = new ApprovedSite(); + site1.setId(1L); + site1.setClientId("foo"); + site1.setCreationDate(creationDate1); + site1.setAccessDate(accessDate1); + site1.setUserId("user1"); + site1.setAllowedScopes(ImmutableSet.of("openid", "phone")); + when(mockToken1.getApprovedSite()).thenReturn(site1); + + Date creationDate2 = formatter.parse("2014-09-11T18:49:44.090+00:00", Locale.ENGLISH); + Date accessDate2 = formatter.parse("2014-09-11T20:49:44.090+00:00", Locale.ENGLISH); + Date timeoutDate2 = formatter.parse("2014-10-01T20:49:44.090+00:00", Locale.ENGLISH); + + ApprovedSite site2 = new ApprovedSite(); + site2.setId(2L); + site2.setClientId("bar"); + site2.setCreationDate(creationDate2); + site2.setAccessDate(accessDate2); + site2.setUserId("user2"); + site2.setAllowedScopes(ImmutableSet.of("openid", "offline_access", "email", "profile")); + site2.setTimeoutDate(timeoutDate2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [" + + + "{\"id\":1,\"clientId\":\"foo\",\"creationDate\":\"2014-09-10T22:49:44.090+00:00\",\"accessDate\":\"2014-09-10T23:49:44.090+00:00\"," + + "\"userId\":\"user1\",\"whitelistedSiteId\":null,\"allowedScopes\":[\"openid\",\"phone\"], \"whitelistedSiteId\":1," + + "\"approvedAccessTokens\":[1]}," + + "{\"id\":2,\"clientId\":\"bar\",\"creationDate\":\"2014-09-11T18:49:44.090+00:00\",\"accessDate\":\"2014-09-11T20:49:44.090+00:00\"," + + "\"timeoutDate\":\"2014-10-01T20:49:44.090+00:00\",\"userId\":\"user2\"," + + "\"allowedScopes\":[\"openid\",\"offline_access\",\"email\",\"profile\"]}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(approvedSiteRepository.save(isA(ApprovedSite.class))).thenAnswer(new Answer() { + Long id = 364L; + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + ApprovedSite _site = (ApprovedSite) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + when(approvedSiteRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public ApprovedSite answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeDb.get(_id); + } + }); + when(wlSiteRepository.getById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 432L; + @Override + public WhitelistedSite answer(InvocationOnMock invocation) throws Throwable { + WhitelistedSite _site = mock(WhitelistedSite.class); + when(_site.getId()).thenReturn(id++); + return _site; + } + }); + when(tokenRepository.getAccessTokenById(isNull(Long.class))).thenAnswer(new Answer() { + Long id = 245L; + @Override + public OAuth2AccessTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2AccessTokenEntity _token = mock(OAuth2AccessTokenEntity.class); + when(_token.getId()).thenReturn(id++); + return _token; + } + }); + + dataService.importData(reader); + //2 for sites, 1 for updating access token ref on #1 + verify(approvedSiteRepository, times(3)).save(capturedApprovedSites.capture()); + + List savedSites = new ArrayList(fakeDb.values()); + + assertThat(savedSites.size(), is(2)); + + assertThat(savedSites.get(0).getClientId(), equalTo(site1.getClientId())); + assertThat(savedSites.get(0).getAccessDate(), equalTo(site1.getAccessDate())); + assertThat(savedSites.get(0).getCreationDate(), equalTo(site1.getCreationDate())); + assertThat(savedSites.get(0).getAllowedScopes(), equalTo(site1.getAllowedScopes())); + assertThat(savedSites.get(0).getTimeoutDate(), equalTo(site1.getTimeoutDate())); + + assertThat(savedSites.get(1).getClientId(), equalTo(site2.getClientId())); + assertThat(savedSites.get(1).getAccessDate(), equalTo(site2.getAccessDate())); + assertThat(savedSites.get(1).getCreationDate(), equalTo(site2.getCreationDate())); + assertThat(savedSites.get(1).getAllowedScopes(), equalTo(site2.getAllowedScopes())); + assertThat(savedSites.get(1).getTimeoutDate(), equalTo(site2.getTimeoutDate())); + } + + @Test + public void testExportAuthenticationHolders() throws IOException { + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = new UsernamePasswordAuthenticationToken("user1", "pass1", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, null); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + List allAuthHolders = ImmutableList.of(holder1, holder2); + + when(clientRepository.getAllClients()).thenReturn(new HashSet()); + when(approvedSiteRepository.getAll()).thenReturn(new HashSet()); + when(wlSiteRepository.getAll()).thenReturn(new HashSet()); + when(blSiteRepository.getAll()).thenReturn(new HashSet()); + when(authHolderRepository.getAll()).thenReturn(allAuthHolders); + when(tokenRepository.getAllAccessTokens()).thenReturn(new HashSet()); + when(tokenRepository.getAllRefreshTokens()).thenReturn(new HashSet()); + when(sysScopeRepository.getAll()).thenReturn(new HashSet()); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + + // check our holder list (this test) + JsonArray holders = config.get(MITREidDataService.AUTHENTICATIONHOLDERS).getAsJsonArray(); + + assertThat(holders.size(), is(2)); + // check for both of our clients in turn + Set checked = new HashSet<>(); + for (JsonElement e : holders) { + assertThat(e.isJsonObject(), is(true)); + JsonObject holder = e.getAsJsonObject(); + + AuthenticationHolderEntity compare = null; + if (holder.get("id").getAsLong() == holder1.getId()) { + compare = holder1; + } else if (holder.get("id").getAsLong() == holder2.getId()) { + compare = holder2; + } + + if (compare == null) { + fail("Could not find matching authentication holder id: " + holder.get("id").getAsString()); + } else { + assertTrue(holder.get("clientId").getAsString().equals(compare.getClientId())); + assertTrue(holder.get("approved").getAsBoolean() == compare.isApproved()); + assertTrue(holder.get("redirectUri").getAsString().equals(compare.getRedirectUri())); + if (compare.getUserAuth() != null) { + assertTrue(holder.get("savedUserAuthentication").isJsonObject()); + JsonObject savedAuth = holder.get("savedUserAuthentication").getAsJsonObject(); + assertTrue(savedAuth.get("name").getAsString().equals(compare.getUserAuth().getName())); + assertTrue(savedAuth.get("authenticated").getAsBoolean() == compare.getUserAuth().isAuthenticated()); + assertTrue(savedAuth.get("sourceClass").getAsString().equals(compare.getUserAuth().getSourceClass())); + } + checked.add(compare); + } + } + // make sure all of our clients were found + assertThat(checked.containsAll(allAuthHolders), is(true)); + } + + @Test + public void testImportAuthenticationHolders() throws IOException { + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"," + + "\"savedUserAuthentication\":null}," + + "{\"id\":2,\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"," + + "\"savedUserAuthentication\":null}" + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + final Map fakeDb = new HashMap<>(); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 243L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _site = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_site.getId() == null) { + _site.setId(id++); + } + fakeDb.put(_site.getId(), _site); + return _site; + } + }); + + dataService.importData(reader); + verify(authHolderRepository, times(2)).save(capturedAuthHolders.capture()); + + List savedAuthHolders = capturedAuthHolders.getAllValues(); + + assertThat(savedAuthHolders.size(), is(2)); + assertThat(savedAuthHolders.get(0).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder1.getAuthentication().getOAuth2Request().getClientId())); + assertThat(savedAuthHolders.get(1).getAuthentication().getOAuth2Request().getClientId(), equalTo(holder2.getAuthentication().getOAuth2Request().getClientId())); + } + + @Test + public void testExportSystemScopes() throws IOException { + SystemScope scope1 = new SystemScope(); + scope1.setId(1L); + scope1.setValue("scope1"); + scope1.setDescription("Scope 1"); + scope1.setRestricted(true); + scope1.setDefaultScope(false); + scope1.setIcon("glass"); + + SystemScope scope2 = new SystemScope(); + scope2.setId(2L); + scope2.setValue("scope2"); + scope2.setDescription("Scope 2"); + scope2.setRestricted(false); + scope2.setDefaultScope(false); + scope2.setIcon("ball"); + + SystemScope scope3 = new SystemScope(); + scope3.setId(3L); + scope3.setValue("scope3"); + scope3.setDescription("Scope 3"); + scope3.setRestricted(false); + scope3.setDefaultScope(true); + scope3.setIcon("road"); + + Set allScopes = ImmutableSet.of(scope1, scope2, scope3); + + Mockito.when(clientRepository.getAllClients()).thenReturn(new HashSet()); + Mockito.when(approvedSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(wlSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(blSiteRepository.getAll()).thenReturn(new HashSet()); + Mockito.when(authHolderRepository.getAll()).thenReturn(new ArrayList()); + Mockito.when(tokenRepository.getAllAccessTokens()).thenReturn(new HashSet()); + Mockito.when(tokenRepository.getAllRefreshTokens()).thenReturn(new HashSet()); + Mockito.when(sysScopeRepository.getAll()).thenReturn(allScopes); + + // do the data export + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + dataService.exportData(writer); + writer.endObject(); + writer.close(); + + // parse the output as a JSON object for testing + JsonElement elem = new JsonParser().parse(stringWriter.toString()); + JsonObject root = elem.getAsJsonObject(); + + // make sure the root is there + assertThat(root.has(MITREidDataService.MITREID_CONNECT_1_3), is(true)); + + JsonObject config = root.get(MITREidDataService.MITREID_CONNECT_1_3).getAsJsonObject(); + + // make sure all the root elements are there + assertThat(config.has(MITREidDataService.CLIENTS), is(true)); + assertThat(config.has(MITREidDataService.GRANTS), is(true)); + assertThat(config.has(MITREidDataService.WHITELISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.BLACKLISTEDSITES), is(true)); + assertThat(config.has(MITREidDataService.REFRESHTOKENS), is(true)); + assertThat(config.has(MITREidDataService.ACCESSTOKENS), is(true)); + assertThat(config.has(MITREidDataService.SYSTEMSCOPES), is(true)); + assertThat(config.has(MITREidDataService.AUTHENTICATIONHOLDERS), is(true)); + + // make sure the root elements are all arrays + assertThat(config.get(MITREidDataService.CLIENTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.GRANTS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.WHITELISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.BLACKLISTEDSITES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.REFRESHTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.ACCESSTOKENS).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.SYSTEMSCOPES).isJsonArray(), is(true)); + assertThat(config.get(MITREidDataService.AUTHENTICATIONHOLDERS).isJsonArray(), is(true)); + + + // check our scope list (this test) + JsonArray scopes = config.get(MITREidDataService.SYSTEMSCOPES).getAsJsonArray(); + + assertThat(scopes.size(), is(3)); + // check for both of our clients in turn + Set checked = new HashSet<>(); + for (JsonElement e : scopes) { + assertThat(e.isJsonObject(), is(true)); + JsonObject scope = e.getAsJsonObject(); + + SystemScope compare = null; + if (scope.get("value").getAsString().equals(scope1.getValue())) { + compare = scope1; + } else if (scope.get("value").getAsString().equals(scope2.getValue())) { + compare = scope2; + } else if (scope.get("value").getAsString().equals(scope3.getValue())) { + compare = scope3; + } + + if (compare == null) { + fail("Could not find matching scope value: " + scope.get("value").getAsString()); + } else { + assertThat(scope.get("value").getAsString(), equalTo(compare.getValue())); + assertThat(scope.get("description").getAsString(), equalTo(compare.getDescription())); + assertThat(scope.get("icon").getAsString(), equalTo(compare.getIcon())); + assertThat(scope.get("restricted").getAsBoolean(), equalTo(compare.isRestricted())); + assertThat(scope.get("defaultScope").getAsBoolean(), equalTo(compare.isDefaultScope())); + checked.add(compare); + } + } + // make sure all of our clients were found + assertThat(checked.containsAll(allScopes), is(true)); + + } + + @Test + public void testImportSystemScopes() throws IOException { + SystemScope scope1 = new SystemScope(); + scope1.setId(1L); + scope1.setValue("scope1"); + scope1.setDescription("Scope 1"); + scope1.setRestricted(true); + scope1.setDefaultScope(false); + scope1.setIcon("glass"); + + SystemScope scope2 = new SystemScope(); + scope2.setId(2L); + scope2.setValue("scope2"); + scope2.setDescription("Scope 2"); + scope2.setRestricted(false); + scope2.setDefaultScope(false); + scope2.setIcon("ball"); + + SystemScope scope3 = new SystemScope(); + scope3.setId(3L); + scope3.setValue("scope3"); + scope3.setDescription("Scope 3"); + scope3.setRestricted(false); + scope3.setDefaultScope(true); + scope3.setIcon("road"); + + String configJson = "{" + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [], " + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [" + + + "{\"id\":1,\"description\":\"Scope 1\",\"icon\":\"glass\",\"value\":\"scope1\",\"restricted\":true,\"defaultScope\":false}," + + "{\"id\":2,\"description\":\"Scope 2\",\"icon\":\"ball\",\"value\":\"scope2\",\"restricted\":false,\"defaultScope\":false}," + + "{\"id\":3,\"description\":\"Scope 3\",\"icon\":\"road\",\"value\":\"scope3\",\"restricted\":false,\"defaultScope\":true}" + + + " ]" + + "}"; + + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + + dataService.importData(reader); + verify(sysScopeRepository, times(3)).save(capturedScope.capture()); + + List savedScopes = capturedScope.getAllValues(); + + assertThat(savedScopes.size(), is(3)); + assertThat(savedScopes.get(0).getValue(), equalTo(scope1.getValue())); + assertThat(savedScopes.get(0).getDescription(), equalTo(scope1.getDescription())); + assertThat(savedScopes.get(0).getIcon(), equalTo(scope1.getIcon())); + assertThat(savedScopes.get(0).isDefaultScope(), equalTo(scope1.isDefaultScope())); + assertThat(savedScopes.get(0).isRestricted(), equalTo(scope1.isRestricted())); + + assertThat(savedScopes.get(1).getValue(), equalTo(scope2.getValue())); + assertThat(savedScopes.get(1).getDescription(), equalTo(scope2.getDescription())); + assertThat(savedScopes.get(1).getIcon(), equalTo(scope2.getIcon())); + assertThat(savedScopes.get(1).isDefaultScope(), equalTo(scope2.isDefaultScope())); + assertThat(savedScopes.get(1).isRestricted(), equalTo(scope2.isRestricted())); + + assertThat(savedScopes.get(2).getValue(), equalTo(scope3.getValue())); + assertThat(savedScopes.get(2).getDescription(), equalTo(scope3.getDescription())); + assertThat(savedScopes.get(2).getIcon(), equalTo(scope3.getIcon())); + assertThat(savedScopes.get(2).isDefaultScope(), equalTo(scope3.isDefaultScope())); + assertThat(savedScopes.get(2).isRestricted(), equalTo(scope3.isRestricted())); + + } + + @Test + public void testFixRefreshTokenAuthHolderReferencesOnImport() throws IOException, ParseException { + String expiration1 = "2014-09-10T22:49:44.090+00:00"; + Date expirationDate1 = formatter.parse(expiration1, Locale.ENGLISH); + + ClientDetailsEntity mockedClient1 = mock(ClientDetailsEntity.class); + when(mockedClient1.getClientId()).thenReturn("mocked_client_1"); + + OAuth2Request req1 = new OAuth2Request(new HashMap(), "client1", new ArrayList(), + true, new HashSet(), new HashSet(), "http://foo.com", + new HashSet(), null); + Authentication mockAuth1 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth1 = new OAuth2Authentication(req1, mockAuth1); + + AuthenticationHolderEntity holder1 = new AuthenticationHolderEntity(); + holder1.setId(1L); + holder1.setAuthentication(auth1); + + OAuth2RefreshTokenEntity token1 = new OAuth2RefreshTokenEntity(); + token1.setId(1L); + token1.setClient(mockedClient1); + token1.setExpiration(expirationDate1); + token1.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.")); + token1.setAuthenticationHolder(holder1); + + String expiration2 = "2015-01-07T18:31:50.079+00:00"; + Date expirationDate2 = formatter.parse(expiration2, Locale.ENGLISH); + + ClientDetailsEntity mockedClient2 = mock(ClientDetailsEntity.class); + when(mockedClient2.getClientId()).thenReturn("mocked_client_2"); + + OAuth2Request req2 = new OAuth2Request(new HashMap(), "client2", new ArrayList(), + true, new HashSet(), new HashSet(), "http://bar.com", + new HashSet(), null); + Authentication mockAuth2 = mock(Authentication.class, withSettings().serializable()); + OAuth2Authentication auth2 = new OAuth2Authentication(req2, mockAuth2); + + AuthenticationHolderEntity holder2 = new AuthenticationHolderEntity(); + holder2.setId(2L); + holder2.setAuthentication(auth2); + + OAuth2RefreshTokenEntity token2 = new OAuth2RefreshTokenEntity(); + token2.setId(2L); + token2.setClient(mockedClient2); + token2.setExpiration(expirationDate2); + token2.setJwt(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.")); + token2.setAuthenticationHolder(holder2); + + String configJson = "{" + + "\"" + MITREidDataService.SYSTEMSCOPES + "\": [], " + + "\"" + MITREidDataService.ACCESSTOKENS + "\": [], " + + "\"" + MITREidDataService.CLIENTS + "\": [], " + + "\"" + MITREidDataService.GRANTS + "\": [], " + + "\"" + MITREidDataService.WHITELISTEDSITES + "\": [], " + + "\"" + MITREidDataService.BLACKLISTEDSITES + "\": [], " + + "\"" + MITREidDataService.AUTHENTICATIONHOLDERS + "\": [" + + + "{\"id\":1,\"authentication\":{\"authorizationRequest\":{\"clientId\":\"client1\",\"redirectUri\":\"http://foo.com\"}," + + "\"userAuthentication\":null}}," + + "{\"id\":2,\"authentication\":{\"authorizationRequest\":{\"clientId\":\"client2\",\"redirectUri\":\"http://bar.com\"}," + + "\"userAuthentication\":null}}" + + " ]," + + "\"" + MITREidDataService.REFRESHTOKENS + "\": [" + + + "{\"id\":1,\"clientId\":\"mocked_client_1\",\"expiration\":\"2014-09-10T22:49:44.090+00:00\"," + + "\"authenticationHolderId\":1,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJmOTg4OWQyOS0xMTk1LTQ4ODEtODgwZC1lZjVlYzAwY2Y4NDIifQ.\"}," + + "{\"id\":2,\"clientId\":\"mocked_client_2\",\"expiration\":\"2015-01-07T18:31:50.079+00:00\"," + + "\"authenticationHolderId\":2,\"value\":\"eyJhbGciOiJub25lIn0.eyJqdGkiOiJlYmEyYjc3My0xNjAzLTRmNDAtOWQ3MS1hMGIxZDg1OWE2MDAifQ.\"}" + + + " ]" + + "}"; + logger.debug(configJson); + + JsonReader reader = new JsonReader(new StringReader(configJson)); + final Map fakeRefreshTokenTable = new HashMap<>(); + final Map fakeAuthHolderTable = new HashMap<>(); + when(tokenRepository.saveRefreshToken(isA(OAuth2RefreshTokenEntity.class))).thenAnswer(new Answer() { + Long id = 343L; + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + OAuth2RefreshTokenEntity _token = (OAuth2RefreshTokenEntity) invocation.getArguments()[0]; + if(_token.getId() == null) { + _token.setId(id++); + } + fakeRefreshTokenTable.put(_token.getId(), _token); + return _token; + } + }); + when(tokenRepository.getRefreshTokenById(anyLong())).thenAnswer(new Answer() { + @Override + public OAuth2RefreshTokenEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeRefreshTokenTable.get(_id); + } + }); + when(clientRepository.getClientByClientId(anyString())).thenAnswer(new Answer() { + @Override + public ClientDetailsEntity answer(InvocationOnMock invocation) throws Throwable { + String _clientId = (String) invocation.getArguments()[0]; + ClientDetailsEntity _client = mock(ClientDetailsEntity.class); + when(_client.getClientId()).thenReturn(_clientId); + return _client; + } + }); + when(authHolderRepository.save(isA(AuthenticationHolderEntity.class))).thenAnswer(new Answer() { + Long id = 356L; + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + AuthenticationHolderEntity _holder = (AuthenticationHolderEntity) invocation.getArguments()[0]; + if(_holder.getId() == null) { + _holder.setId(id++); + } + fakeAuthHolderTable.put(_holder.getId(), _holder); + return _holder; + } + }); + when(authHolderRepository.getById(anyLong())).thenAnswer(new Answer() { + @Override + public AuthenticationHolderEntity answer(InvocationOnMock invocation) throws Throwable { + Long _id = (Long) invocation.getArguments()[0]; + return fakeAuthHolderTable.get(_id); + } + }); + dataService.importData(reader); + + List savedRefreshTokens = new ArrayList(fakeRefreshTokenTable.values()); //capturedRefreshTokens.getAllValues(); + Collections.sort(savedRefreshTokens, new refreshTokenIdComparator()); + + assertThat(savedRefreshTokens.get(0).getAuthenticationHolder().getId(), equalTo(356L)); + assertThat(savedRefreshTokens.get(1).getAuthenticationHolder().getId(), equalTo(357L)); + } + + private Set jsonArrayToStringSet(JsonArray a) { + Set s = new HashSet<>(); + for (JsonElement jsonElement : a) { + s.add(jsonElement.getAsString()); + } + return s; + } + +} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestUUIDPairwiseIdentiferService.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestUUIDPairwiseIdentiferService.java index 474efe804e..30e9f5f514 100644 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestUUIDPairwiseIdentiferService.java +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/service/impl/TestUUIDPairwiseIdentiferService.java @@ -1,6 +1,7 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +16,10 @@ * limitations under the License. *******************************************************************************/ /** - * + * */ package org.mitre.openid.connect.service.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotSame; - import java.util.Set; import java.util.UUID; @@ -42,6 +40,9 @@ import com.google.common.collect.ImmutableSet; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + /** * @author jricher * diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/token/TestConnectTokenEnhancer.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/token/TestConnectTokenEnhancer.java new file mode 100644 index 0000000000..7e1acbfd99 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/token/TestConnectTokenEnhancer.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * + * 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 org.mitre.openid.connect.token; + +import java.text.ParseException; + +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.model.UserInfo; +import org.mitre.openid.connect.service.OIDCTokenService; +import org.mitre.openid.connect.service.UserInfoService; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet.Builder; + +@RunWith(MockitoJUnitRunner.class) +public class TestConnectTokenEnhancer { + + private static final String CLIENT_ID = "client"; + private static final String KEY_ID = "key"; + + private ConfigurationPropertiesBean configBean = new ConfigurationPropertiesBean(); + + @Mock + private JWTSigningAndValidationService jwtService; + + @Mock + private ClientDetailsEntityService clientService; + + @Mock + private UserInfoService userInfoService; + + @Mock + private OIDCTokenService connectTokenService; + + @Mock + private OAuth2Authentication authentication; + + private OAuth2Request request = new OAuth2Request(CLIENT_ID) { }; + + @InjectMocks + private ConnectTokenEnhancer enhancer = new ConnectTokenEnhancer(); + + @Before + public void prepare() { + configBean.setIssuer("https://auth.example.org/"); + enhancer.setConfigBean(configBean); + + ClientDetailsEntity client = new ClientDetailsEntity(); + client.setClientId(CLIENT_ID); + Mockito.when(clientService.loadClientByClientId(Mockito.anyString())).thenReturn(client); + Mockito.when(authentication.getOAuth2Request()).thenReturn(request); + Mockito.when(jwtService.getDefaultSigningAlgorithm()).thenReturn(JWSAlgorithm.RS256); + Mockito.when(jwtService.getDefaultSignerKeyId()).thenReturn(KEY_ID); + } + + @Test + public void invokesCustomClaimsHook() throws ParseException { + configure(enhancer = new ConnectTokenEnhancer() { + @Override + protected void addCustomAccessTokenClaims(Builder builder, OAuth2AccessTokenEntity token, + OAuth2Authentication authentication) { + builder.claim("test", "foo"); + } + }); + + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + + OAuth2AccessTokenEntity enhanced = (OAuth2AccessTokenEntity) enhancer.enhance(token, authentication); + Assert.assertEquals("foo", enhanced.getJwt().getJWTClaimsSet().getClaim("test")); + } + + private void configure(ConnectTokenEnhancer e) { + e.setConfigBean(configBean); + e.setJwtService(jwtService); + e.setClientService(clientService); + } +} diff --git a/openid-connect-server/src/test/java/org/mitre/openid/connect/util/TestIdTokenHashUtils.java b/openid-connect-server/src/test/java/org/mitre/openid/connect/util/TestIdTokenHashUtils.java index 15026176d0..b8ccbfd4ca 100644 --- a/openid-connect-server/src/test/java/org/mitre/openid/connect/util/TestIdTokenHashUtils.java +++ b/openid-connect-server/src/test/java/org/mitre/openid/connect/util/TestIdTokenHashUtils.java @@ -1,24 +1,23 @@ /******************************************************************************* - * Copyright 2014 The MITRE Corporation - * and the MIT Kerberos and Internet Trust Consortium - * + * Copyright 2018 The MIT Internet Trust Consortium + * + * Portions copyright 2011-2013 The MITRE Corporation + * * 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 org.mitre.openid.connect.util; -import static org.junit.Assert.assertEquals; - import java.text.ParseException; import org.junit.Before; @@ -33,8 +32,10 @@ import com.nimbusds.jose.util.Base64URL; import com.nimbusds.jwt.JWTParser; +import static org.junit.Assert.assertEquals; + /** - * + * * @author wkim * */ @@ -52,13 +53,13 @@ public class TestIdTokenHashUtils { public void prepare() throws ParseException { /* - Claims for first token: - + Claims for first token: + claims.setType("JWT"); claims.setIssuer("www.example.com"); claims.setSubject("example_user"); claims.setClaim("alg", "HS256"); - */ + */ Mockito.when(mockToken256.getJwt()).thenReturn(JWTParser.parse("eyJhbGciOiJub25lIn0.eyJhbGciOiJIUzI1NiIsInN1YiI6ImV4YW1wbGVfdXNlciIsImlzcyI6Ind3dy5leGFtcGxlLmNvbSIsInR5cCI6IkpXVCJ9.")); /* @@ -85,12 +86,7 @@ public void prepare() throws ParseException { @Test public void getAccessTokenHash256() { - /* - * independently generate hash - ascii of token = eyJhbGciOiJub25lIn0.eyJhbGciOiJIUzI1NiIsInN1YiI6ImV4YW1wbGVfdXNlciIsImlzcyI6Ind3dy5leGFtcGxlLmNvbSIsInR5cCI6IkpXVCJ9. - base64url of hash = EP1gXNeESRH-n57baopfTQ - */ - String token = mockToken256.getJwt().serialize(); + mockToken256.getJwt().serialize(); Base64URL expectedHash = new Base64URL("EP1gXNeESRH-n57baopfTQ"); Base64URL resultHash = IdTokenHashUtils.getAccessTokenHash(JWSAlgorithm.HS256, mockToken256); @@ -107,7 +103,7 @@ public void getAccessTokenHash384() { base64url of hash = BWfFK73PQI36M1rg9R6VjMyWOE0-XvBK */ - String token = mockToken384.getJwt().serialize(); + mockToken384.getJwt().serialize(); Base64URL expectedHash = new Base64URL("BWfFK73PQI36M1rg9R6VjMyWOE0-XvBK"); Base64URL resultHash = IdTokenHashUtils.getAccessTokenHash(JWSAlgorithm.ES384, mockToken384); @@ -124,7 +120,7 @@ public void getAccessTokenHash512() { base64url of hash = vGH3QMY-knpACkLgzdkTqu3C9jtvbf2Wk_RSu2vAx8k */ - String token = mockToken512.getJwt().serialize(); + mockToken512.getJwt().serialize(); Base64URL expectedHash = new Base64URL("vGH3QMY-knpACkLgzdkTqu3C9jtvbf2Wk_RSu2vAx8k"); Base64URL resultHash = IdTokenHashUtils.getAccessTokenHash(JWSAlgorithm.RS512, mockToken512); diff --git a/openid-connect-server/src/test/resources/resources/js/locale/en/messages.json b/openid-connect-server/src/test/resources/resources/js/locale/en/messages.json new file mode 100644 index 0000000000..6c5b3d3cbc --- /dev/null +++ b/openid-connect-server/src/test/resources/resources/js/locale/en/messages.json @@ -0,0 +1,3 @@ +{ + "testAttribute": "testValue" +} diff --git a/pom.xml b/pom.xml index 2ceeb331cb..3c223dae63 100644 --- a/pom.xml +++ b/pom.xml @@ -1,46 +1,51 @@ - 4.0.0 - org.mitre - openid-connect-parent - 1.1.10-SNAPSHOT - MITREid Connect - pom - - org.sonatype.oss - oss-parent - 7 - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - A business-friendly OSS license - - - - openid-connect-common - openid-connect-client + Copyright 2018 The MIT Internet Trust Consortium + + Portions copyright 2011-2013 The MITRE Corporation + + 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. + --> + + 4.0.0 + org.mitre + openid-connect-parent + 1.3.5-SNAPSHOT + MITREid Connect + pom + + org.sonatype.oss + oss-parent + 9 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + openid-connect-common + openid-connect-client openid-connect-server - openid-connect-server-webapp - - + openid-connect-server-webapp + uma-server + uma-server-webapp + + scm:git:https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server.git scm:git:git@github.com:mitreid-connect/OpenID-Connect-Java-Spring-Server.git @@ -52,12 +57,7 @@ jricher Justin Richer - jricher@mitre.org - - - aanganes - Amanda Anganes - aanganes@mitre.org + jricher@mit.edu @@ -69,58 +69,133 @@ - - 1.6 - 3.2.3.RELEASE - 1.7.2 - 3.2.3.RELEASE - - A reference implementation of OpenID Connect (http://openid.net/connect/) and OAuth 2.0 built on top of Java, Spring, and Spring Security. The project contains a fully functioning server, client, and utility library. - https://github.com/mitreid-connect - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - org.apache.maven.plugins - maven-war-plugin - 2.2 - - - org.apache.maven.plugins - maven-archiver - 2.5 - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9 - - - org.apache.maven.plugins - maven-deploy-plugin - 2.7 - - - org.apache.maven.plugins - maven-source-plugin - 2.2 - - - org.apache.maven.plugins - maven-compiler-plugin - 2.5.1 - - - org.eclipse.jetty - jetty-maven-plugin - 9.1.1.v20140108 - - + + 11 + 1.7.25 + + A reference implementation of OpenID Connect (http://openid.net/connect/), OAuth 2.0, and UMA built on top of Java, Spring, and Spring Security. The project contains a fully functioning server, client, and utility library. + https://github.com/mitreid-connect + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + org.apache.maven.plugins + maven-war-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.10 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + + org.eclipse.jetty + jetty-maven-plugin + 9.4.3.v20170317 + + + true + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + org.appfuse.plugins + warpath-maven-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-site-plugin + 3.6 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.9 + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.appfuse.plugins + + + warpath-maven-plugin + + + [3.5.0,) + + + add-classes + + + + + + + + + + + + ro.isdc.wro4j + wro4j-maven-plugin + 1.10.0 + + + compile + + run + + + + + + ro.isdc.wro4j + wro4j-extensions + 1.10.0 + + + + @@ -134,6 +209,7 @@ MITREid Connect v. ${project.version} MITREid Connect v. ${project.version} ${basedir}/src/main/javadoc/overview.html + -Xdoclint:none @@ -164,166 +240,443 @@ org.apache.maven.plugins maven-compiler-plugin - - true - org.apache.maven.plugins - maven-enforcer-plugin - 1.3 - - - enforce-maven-3 - - enforce - - - - - 3.0.2 - - - true - - - - + + true + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-maven-3 + + enforce + + + + + 3.1.0 + + + true + + + + + + org.jacoco + jacoco-maven-plugin + + + + prepare-agent + + + + report + test + + report + + + + + + org.apache.maven.plugins + maven-site-plugin + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + false + false + + + index + cim + dependencies + dependency-convergence + + dependency-management + help + issue-tracking + license + mailing-list + modules + plugin-management + plugins + project-team + scm + summary + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + true + true + true + MITREid Connect ${project.name} v. ${project.version} + MITREid Connect ${project.name} v. ${project.version} + ${basedir}/src/main/javadoc/overview.html + -Xdoclint:none + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + checkstyle.xml + + + + org.apache.maven.plugins + maven-surefire-plugin + + junit:junit + + **/*_Roo_* + + + + + org.jacoco + jacoco-maven-plugin + + + + - - https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/issues - GitHub Issues - - - Travis CI - https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server - + + https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/issues + GitHub Issues + + + Travis CI + https://travis-ci.org/mitreid-connect/OpenID-Connect-Java-Spring-Server + - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9 - - true - true - true - true - MITREid Connect v. ${project.version} - MITREid Connect v. ${project.version} - ${basedir}/src/main/javadoc/overview.html - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 2.10 - - checkstyle.xml - - - - org.apache.maven.plugins - maven-surefire-plugin - - junit:junit - - **/*_Roo_* - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5.2 - - - html - xml - - - - - + + + + + org.springframework + spring-framework-bom + 5.3.9 + pom + import + - - - - javax.servlet - servlet-api - 2.5 - provided - - - javax.servlet.jsp - jsp-api - 2.1 - provided - - + + + com.fasterxml.jackson.core + jackson-databind + 2.9.8 + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.8 + + + + + org.springframework.security + spring-security-bom + 5.5.2 + pom + import + + + org.springframework.security.oauth + spring-security-oauth2 + 2.1.0.RELEASE + + + + + javax.servlet + jstl + 1.2 + + + javax.servlet + servlet-api + 2.5 + provided + + + javax.servlet.jsp + jsp-api + 2.1 + provided + - - - junit - junit - 4.7 - test - - - org.easymock - easymock - 2.0 - test - - - org.springframework - spring-test - ${org.springframework-version} - test - + + + mysql + mysql-connector-java + 5.1.42 + + + org.hsqldb + hsqldb + 2.3.4 + + + org.postgresql + postgresql + 42.0.0.jre7 + + + com.oracle + ojdbc6 + 11.1.0.7.0 + + + org.eclipse.persistence + org.eclipse.persistence.jpa + 2.7.4 + + + org.eclipse.persistence + javax.persistence + 2.2.1 + + + com.zaxxer + HikariCP + 2.6.1 + + + + + + org.slf4j + slf4j-api + ${org.slf4j-version} + + + org.slf4j + slf4j-jdk14 + test + ${org.slf4j-version} + + + org.slf4j + jcl-over-slf4j + ${org.slf4j-version} + + + org.slf4j + slf4j-log4j12 + ${org.slf4j-version} + runtime + + + log4j + log4j + 1.2.17 + + + javax.mail + mail + + + javax.jms + jms + + + com.sun.jdmk + jmxtools + + + com.sun.jmx + jmxri + + + runtime + + + + + junit + junit + 4.12 + test + + + org.easymock + easymock + 3.4 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + + org.mitre + openid-connect-common + ${project.version} + + + org.mitre + openid-connect-client + ${project.version} + + + org.mitre + openid-connect-server + ${project.version} + + + org.mitre + openid-connect-server-webapp + ${project.version} + war + + + org.mitre + openid-connect-server-webapp + ${project.version} + warpath + + + org.mitre + uma-server + ${project.version} + + + org.mitre + uma-server-webapp + ${project.version} + war + + + + + com.google.guava + guava + 27.0-jre + + + com.google.code.gson + gson + 2.8.0 + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + commons-logging + commons-logging + + + + + com.nimbusds + nimbus-jose-jwt + 5.4 + + + org.bouncycastle + bcprov-jdk15on + [1.52,) + + + org.eclipse.persistence + org.eclipse.persistence.core + 2.7.4 + + + org.apache.commons + commons-io + 1.3.2 + + + ro.isdc.wro4j + wro4j-extensions + 1.10.0 + + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + jakarta.xml.bind + jakarta.xml.bind-api + 3.0.0 + + + javax.xml.bind + jaxb-api + 2.3.1 + + + javax.activation + activation + 1.1 + + + org.glassfish.jaxb + jaxb-runtime + 2.3.0-b170127.1453 + + + + + + + + junit + junit + + + org.easymock + easymock + + + org.springframework + spring-test + + + commons-logging + commons-logging + + + org.mockito mockito-all - 1.9.5 - test - - - - org.slf4j - slf4j-jdk14 - test - ${org.slf4j-version} - + + org.slf4j + slf4j-jdk14 + + + org.slf4j + jcl-over-slf4j + + + javax.servlet + servlet-api + + + javax.servlet.jsp + jsp-api + - - - spring-milestone - Spring Maven MILESTONE Repository - http://maven.springframework.org/milestone - - - - - - - 1.8 - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9 - - -Xdoclint:none - - - - - - - - diff --git a/uma-server-webapp/pom.xml b/uma-server-webapp/pom.xml new file mode 100644 index 0000000000..a1db8a2739 --- /dev/null +++ b/uma-server-webapp/pom.xml @@ -0,0 +1,97 @@ + + + + 4.0.0 + + org.mitre + openid-connect-parent + 1.3.5-SNAPSHOT + .. + + uma-server-webapp + war + UMA Server Webapp + Deployable package of the User Managed Access (UMA) server extension to MITREid Connect + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java-version} + ${java-version} + + + + org.appfuse.plugins + warpath-maven-plugin + true + + + + add-classes + + + + + + org.apache.maven.plugins + maven-war-plugin + + uma-server-webapp + + + org.mitre + openid-connect-server-webapp + + + false + + + + org.eclipse.jetty + jetty-maven-plugin + + ${project.build.directory}/uma-server-webapp.war + + /uma-server-webapp + + + + + + + + org.mitre + openid-connect-server-webapp + war + + + org.mitre + openid-connect-server-webapp + warpath + + + org.mitre + uma-server + + + org.mitre + openid-connect-client + + + diff --git a/uma-server-webapp/src/main/resources/db/hsql/clients.sql b/uma-server-webapp/src/main/resources/db/hsql/clients.sql new file mode 100755 index 0000000000..8d41bcad94 --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/hsql/clients.sql @@ -0,0 +1,77 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT FALSE; + +START TRANSACTION; + +-- +-- Insert client information into the temporary tables. To add clients to the HSQL database, edit things here. +-- + +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('client', 'secret', 'Test Client', false, null, 3600, 600, true), + ('rs', 'secret', 'Test UMA RS', false, null, null, 600, false), + ('c', 'secret', 'Test UMA Client', false, null, null, 600, false); + +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES + ('client', 'openid'), + ('client', 'profile'), + ('client', 'email'), + ('client', 'address'), + ('client', 'phone'), + ('client', 'offline_access'), + ('rs', 'uma_protection'), + ('c', 'uma_authorization'); + +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES + ('client', 'http://localhost/'), + ('client', 'http://localhost:8080/'); + +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES + ('client', 'authorization_code'), + ('client', 'urn:ietf:params:oauth:grant_type:redelegate'), + ('client', 'implicit'), + ('client', 'refresh_token'), + ('rs', 'authorization_code'), + ('rs', 'implicit'), + ('c', 'authorization_code'), + ('c', 'implicit'); + +-- +-- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. +-- + +MERGE INTO client_details + USING (SELECT client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection FROM client_details_TEMP) AS vals(client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) + ON vals.client_id = client_details.client_id + WHEN NOT MATCHED THEN + INSERT (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES(client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection); + +MERGE INTO client_scope + USING (SELECT id, scope FROM client_scope_TEMP, client_details WHERE client_details.client_id = client_scope_TEMP.owner_id) AS vals(id, scope) + ON vals.id = client_scope.owner_id AND vals.scope = client_scope.scope + WHEN NOT MATCHED THEN + INSERT (owner_id, scope) values (vals.id, vals.scope); + +MERGE INTO client_redirect_uri + USING (SELECT id, redirect_uri FROM client_redirect_uri_TEMP, client_details WHERE client_details.client_id = client_redirect_uri_TEMP.owner_id) AS vals(id, redirect_uri) + ON vals.id = client_redirect_uri.owner_id AND vals.redirect_uri = client_redirect_uri.redirect_uri + WHEN NOT MATCHED THEN + INSERT (owner_id, redirect_uri) values (vals.id, vals.redirect_uri); + +MERGE INTO client_grant_type + USING (SELECT id, grant_type FROM client_grant_type_TEMP, client_details WHERE client_details.client_id = client_grant_type_TEMP.owner_id) AS vals(id, grant_type) + ON vals.id = client_grant_type.owner_id AND vals.grant_type = client_grant_type.grant_type + WHEN NOT MATCHED THEN + INSERT (owner_id, grant_type) values (vals.id, vals.grant_type); + +-- +-- Close the transaction and turn autocommit back on +-- + +COMMIT; + +SET AUTOCOMMIT TRUE; + diff --git a/uma-server-webapp/src/main/resources/db/hsql/scopes.sql b/uma-server-webapp/src/main/resources/db/hsql/scopes.sql new file mode 100755 index 0000000000..c3ea0b1133 --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/hsql/scopes.sql @@ -0,0 +1,35 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT FALSE; + +START TRANSACTION; + +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope) VALUES + ('openid', 'log in using your identity', 'user', false, true), + ('profile', 'basic profile information', 'list-alt', false, true), + ('email', 'email address', 'envelope', false, true), + ('address', 'physical address', 'home', false, true), + ('phone', 'telephone number', 'bell', false, true), + ('offline_access', 'offline access', 'time', false, false), + ('uma_protection', 'manage protected resources', 'briefcase', false, false), + ('uma_authorization', 'request access to protected resources', 'share', false, false); + +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +MERGE INTO system_scope + USING (SELECT scope, description, icon, restricted, default_scope FROM system_scope_TEMP) AS vals(scope, description, icon, restricted, default_scope) + ON vals.scope = system_scope.scope + WHEN NOT MATCHED THEN + INSERT (scope, description, icon, restricted, default_scope) VALUES(vals.scope, vals.description, vals.icon, vals.restricted, vals.default_scope); + +COMMIT; + +SET AUTOCOMMIT TRUE; diff --git a/uma-server-webapp/src/main/resources/db/mysql/clients.sql b/uma-server-webapp/src/main/resources/db/mysql/clients.sql new file mode 100755 index 0000000000..02444c4732 --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/mysql/clients.sql @@ -0,0 +1,69 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT = 0; + +START TRANSACTION; + +-- +-- Insert client information into the temporary tables. To add clients to the HSQL database, edit things here. +-- + +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('client', 'secret', 'Test Client', false, null, 3600, 600, true), + ('rs', 'secret', 'Test UMA RS', false, null, null, 600, false), + ('c', 'secret', 'Test UMA Client', false, null, null, 600, false); + +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES + ('client', 'openid'), + ('client', 'profile'), + ('client', 'email'), + ('client', 'address'), + ('client', 'phone'), + ('client', 'offline_access'), + ('rs', 'uma_protection'), + ('c', 'uma_authorization'); + +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES + ('client', 'http://localhost/'), + ('client', 'http://localhost:8080/'); + +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES + ('client', 'authorization_code'), + ('client', 'urn:ietf:params:oauth:grant_type:redelegate'), + ('client', 'implicit'), + ('client', 'refresh_token'), + ('rs', 'authorization_code'), + ('rs', 'implicit'), + ('c', 'authorization_code'), + ('c', 'implicit'); + +-- +-- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. +-- + +INSERT INTO client_details (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) + SELECT client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection FROM client_details_TEMP + ON DUPLICATE KEY UPDATE client_details.client_id = client_details.client_id; + +INSERT INTO client_scope (owner_id, scope) + SELECT id, scope FROM client_scope_TEMP, client_details WHERE client_details.client_id = client_scope_TEMP.owner_id + ON DUPLICATE KEY UPDATE client_scope.owner_id = client_scope.owner_id; + +INSERT INTO client_redirect_uri (owner_id, redirect_uri) + SELECT id, redirect_uri FROM client_redirect_uri_TEMP, client_details WHERE client_details.client_id = client_redirect_uri_TEMP.owner_id + ON DUPLICATE KEY UPDATE client_redirect_uri.owner_id = client_redirect_uri.owner_id; + +INSERT INTO client_grant_type (owner_id, grant_type) + SELECT id, grant_type FROM client_grant_type_TEMP, client_details WHERE client_details.client_id = client_grant_type_TEMP.owner_id + ON DUPLICATE KEY UPDATE client_grant_type.owner_id = client_grant_type.owner_id; + +-- +-- Close the transaction and turn autocommit back on +-- + +COMMIT; + +SET AUTOCOMMIT = 1; + diff --git a/uma-server-webapp/src/main/resources/db/mysql/scopes.sql b/uma-server-webapp/src/main/resources/db/mysql/scopes.sql new file mode 100755 index 0000000000..bdcc0f6e30 --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/mysql/scopes.sql @@ -0,0 +1,33 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +SET AUTOCOMMIT = 0; + +START TRANSACTION; + +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('openid', 'log in using your identity', 'user', false, true, false, null), + ('profile', 'basic profile information', 'list-alt', false, true, false, null), + ('email', 'email address', 'envelope', false, true, false, null), + ('address', 'physical address', 'home', false, true, false, null), + ('phone', 'telephone number', 'bell', false, true, false, null), + ('offline_access', 'offline access', 'time', false, false, false, null), + ('uma_protection', 'manage protected resources', 'briefcase', false, false, false, null), + ('uma_authorization', 'request access to protected resources', 'share', false, false, false, null); + +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +INSERT INTO system_scope (scope, description, icon, restricted, default_scope, structured, structured_param_description) + SELECT scope, description, icon, restricted, default_scope, structured, structured_param_description FROM system_scope_TEMP + ON DUPLICATE KEY UPDATE system_scope.scope = system_scope.scope; + +COMMIT; + +SET AUTOCOMMIT = 1; diff --git a/uma-server-webapp/src/main/resources/db/oracle/clients_oracle.sql b/uma-server-webapp/src/main/resources/db/oracle/clients_oracle.sql new file mode 100755 index 0000000000..783ff2d3a4 --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/oracle/clients_oracle.sql @@ -0,0 +1,61 @@ +-- +-- Insert client information into the temporary tables. To add clients to the Oracle database, edit things here. +-- + +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('client', 'secret', 'Test Client', 0, null, 3600, 600, 1); +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('rs', 'secret', 'Test UMA RS', false, null, null, 600, false); +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('c', 'secret', 'Test UMA Client', false, null, null, 600, false); + +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'openid'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'profile'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'email'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'address'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'phone'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'offline_access'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('rs', 'uma_protection'); +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('c', 'uma_authorization'); + +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES ('client', 'http://localhost/'); +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES ('client', 'http://localhost:8080/'); + +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'authorization_code'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'urn:ietf:params:oauth:grant_type:redelegate'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'implicit'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'refresh_token'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('rs', 'authorization_code'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('rs', 'implicit'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('c', 'authorization_code'); +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('c', 'implicit'); + +-- +-- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. +-- + +MERGE INTO client_details + USING (SELECT client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection FROM client_details_TEMP) vals + ON (vals.client_id = client_details.client_id) + WHEN NOT MATCHED THEN + INSERT (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, + id_token_validity_seconds, allow_introspection) VALUES(client_details_seq.nextval, vals.client_id, vals.client_secret, vals.client_name, vals.dynamically_registered, + vals.refresh_token_validity_seconds, vals.access_token_validity_seconds, vals.id_token_validity_seconds, vals.allow_introspection); + +MERGE INTO client_scope + USING (SELECT id, scope FROM client_scope_TEMP, client_details WHERE client_details.client_id = client_scope_TEMP.owner_id) vals + ON (vals.id = client_scope.owner_id AND vals.scope = client_scope.scope) + WHEN NOT MATCHED THEN + INSERT (owner_id, scope) values (vals.id, vals.scope); + +MERGE INTO client_redirect_uri + USING (SELECT id, redirect_uri FROM client_redirect_uri_TEMP, client_details WHERE client_details.client_id = client_redirect_uri_TEMP.owner_id) vals + ON (vals.id = client_redirect_uri.owner_id AND vals.redirect_uri = client_redirect_uri.redirect_uri) + WHEN NOT MATCHED THEN + INSERT (owner_id, redirect_uri) values (vals.id, vals.redirect_uri); + +MERGE INTO client_grant_type + USING (SELECT id, grant_type FROM client_grant_type_TEMP, client_details WHERE client_details.client_id = client_grant_type_TEMP.owner_id) vals + ON (vals.id = client_grant_type.owner_id AND vals.grant_type = client_grant_type.grant_type) + WHEN NOT MATCHED THEN + INSERT (owner_id, grant_type) values (vals.id, vals.grant_type); diff --git a/uma-server-webapp/src/main/resources/db/oracle/scopes_oracle.sql b/uma-server-webapp/src/main/resources/db/oracle/scopes_oracle.sql new file mode 100755 index 0000000000..a52e021dea --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/oracle/scopes_oracle.sql @@ -0,0 +1,31 @@ +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('openid', 'log in using your identity', 'user', 0, 1, 0, null); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('profile', 'basic profile information', 'list-alt', 0, 1, 0, null); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('email', 'email address', 'envelope', 0, 1, 0, null); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('address', 'physical address', 'home', 0, 1, 0, null); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('phone', 'telephone number', 'bell', 0, 1, 0, null); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('offline_access', 'offline access', 'time', 0, 0, 0, null); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('uma_protection', 'manage protected resources', 'briefcase', 0, 0, 0, null); +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('uma_authorization', 'request access to protected resources', 'share', 0, 0, 0, null); + +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +MERGE INTO system_scope + USING (SELECT scope, description, icon, restricted, default_scope, structured, structured_param_description FROM system_scope_TEMP) vals + ON (vals.scope = system_scope.scope) + WHEN NOT MATCHED THEN + INSERT (id, scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES(system_scope_seq.nextval, vals.scope, + vals.description, vals.icon, vals.restricted, vals.default_scope, vals.structured, vals.structured_param_description); diff --git a/uma-server-webapp/src/main/resources/db/psql/clients.sql b/uma-server-webapp/src/main/resources/db/psql/clients.sql new file mode 100755 index 0000000000..d4c75e7fe6 --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/psql/clients.sql @@ -0,0 +1,74 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +--SET AUTOCOMMIT = OFF; + +START TRANSACTION; + +-- +-- Insert client information into the temporary tables. To add clients to the HSQL database, edit things here. +-- + +INSERT INTO client_details_TEMP (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) VALUES + ('client', 'secret', 'Test Client', false, null, 3600, 600, true), + ('rs', 'secret', 'Test UMA RS', false, null, null, 600, false), + ('c', 'secret', 'Test UMA Client', false, null, null, 600, false); + +INSERT INTO client_scope_TEMP (owner_id, scope) VALUES + ('client', 'openid'), + ('client', 'profile'), + ('client', 'email'), + ('client', 'address'), + ('client', 'phone'), + ('client', 'offline_access'), + ('rs', 'uma_protection'), + ('c', 'uma_authorization'); + +INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES + ('client', 'http://localhost/'), + ('client', 'http://localhost:8080/'); + +INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES + ('client', 'authorization_code'), + ('client', 'urn:ietf:params:oauth:grant_type:redelegate'), + ('client', 'implicit'), + ('client', 'refresh_token'), + ('rs', 'authorization_code'), + ('rs', 'implicit'), + ('c', 'authorization_code'), + ('c', 'implicit'); + +-- +-- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. +-- + +INSERT INTO client_details (client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection) + SELECT client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection FROM client_details_TEMP + ON CONFLICT + DO NOTHING; + +INSERT INTO client_scope (scope) + SELECT scope FROM client_scope_TEMP, client_details WHERE client_details.client_id = client_scope_TEMP.owner_id + ON CONFLICT + DO NOTHING; + +INSERT INTO client_redirect_uri (redirect_uri) + SELECT redirect_uri FROM client_redirect_uri_TEMP, client_details WHERE client_details.client_id = client_redirect_uri_TEMP.owner_id + ON CONFLICT + DO NOTHING; + +INSERT INTO client_grant_type (grant_type) + SELECT grant_type FROM client_grant_type_TEMP, client_details WHERE client_details.client_id = client_grant_type_TEMP.owner_id + ON CONFLICT + DO NOTHING; + +-- +-- Close the transaction and turn autocommit back on +-- + +COMMIT; + +--SET AUTOCOMMIT = ON; + + diff --git a/uma-server-webapp/src/main/resources/db/psql/scopes.sql b/uma-server-webapp/src/main/resources/db/psql/scopes.sql new file mode 100755 index 0000000000..8b2611b832 --- /dev/null +++ b/uma-server-webapp/src/main/resources/db/psql/scopes.sql @@ -0,0 +1,33 @@ +-- +-- Turn off autocommit and start a transaction so that we can use the temp tables +-- + +--SET AUTOCOMMIT = OFF; + +START TRANSACTION; + +-- +-- Insert scope information into the temporary tables. +-- + +INSERT INTO system_scope_TEMP (scope, description, icon, restricted, default_scope, structured, structured_param_description) VALUES + ('openid', 'log in using your identity', 'user', false, true, false, null), + ('profile', 'basic profile information', 'list-alt', false, true, false, null), + ('email', 'email address', 'envelope', false, true, false, null), + ('address', 'physical address', 'home', false, true, false, null), + ('phone', 'telephone number', 'bell', false, true, false, null), + ('offline_access', 'offline access', 'time', false, false, false, null); + +-- +-- Merge the temporary scopes safely into the database. This is a two-step process to keep scopes from being created on every startup with a persistent store. +-- + +INSERT INTO system_scope (scope, description, icon, restricted, default_scope, structured, structured_param_description) + SELECT scope, description, icon, restricted, default_scope, structured, structured_param_description FROM system_scope_TEMP + ON CONFLICT(scope) + DO NOTHING; + +COMMIT; + +--SET AUTOCOMMIT = ON; + diff --git a/uma-server-webapp/src/main/webapp/WEB-INF/endpoint-config.xml b/uma-server-webapp/src/main/webapp/WEB-INF/endpoint-config.xml new file mode 100644 index 0000000000..7c645d23a8 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/WEB-INF/endpoint-config.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/uma-server-webapp/src/main/webapp/WEB-INF/server-config.xml b/uma-server-webapp/src/main/webapp/WEB-INF/server-config.xml new file mode 100644 index 0000000000..92685552c6 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/WEB-INF/server-config.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + uma + messages + + + + + + + + + + + + diff --git a/uma-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag b/uma-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag new file mode 100644 index 0000000000..47df4a3615 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag @@ -0,0 +1,21 @@ +<%@ tag language="java" pageEncoding="UTF-8"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> +<%@ taglib prefix="security" + uri="http://www.springframework.org/security/tags"%> + + +
  • +
  • +
  • +
  • +
  • +
    + +
  • +
  • +
  • +
  • +
  • + +
  • +
  • \ No newline at end of file diff --git a/uma-server-webapp/src/main/webapp/WEB-INF/ui-config.xml b/uma-server-webapp/src/main/webapp/WEB-INF/ui-config.xml new file mode 100644 index 0000000000..2cd7bfc33b --- /dev/null +++ b/uma-server-webapp/src/main/webapp/WEB-INF/ui-config.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + resources/js/client.js + resources/js/grant.js + resources/js/scope.js + resources/js/whitelist.js + resources/js/dynreg.js + resources/js/rsreg.js + resources/js/token.js + resources/js/blacklist.js + resources/js/profile.js + resources/js/policy.js + + + + + + diff --git a/uma-server-webapp/src/main/webapp/WEB-INF/user-context.xml b/uma-server-webapp/src/main/webapp/WEB-INF/user-context.xml new file mode 100644 index 0000000000..4a2f7bb0d3 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/WEB-INF/user-context.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + openid + profile + email + phone + address + + + + + + #{configBean.issuer + "openid_connect_login"} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/uma-server-webapp/src/main/webapp/WEB-INF/views/external_login.jsp b/uma-server-webapp/src/main/webapp/WEB-INF/views/external_login.jsp new file mode 100644 index 0000000000..897afdaaa1 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/WEB-INF/views/external_login.jsp @@ -0,0 +1,42 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> + + +
    +
    +
    + +

    Log In

    + +

    Enter your email address to log in

    + +
    +
    + +
    + + + + +
    + +
    + +
    +
    +
    + + + \ No newline at end of file diff --git a/uma-server-webapp/src/main/webapp/resources/js/locale/en/uma.json b/uma-server-webapp/src/main/webapp/resources/js/locale/en/uma.json new file mode 100644 index 0000000000..69ff2e1869 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/resources/js/locale/en/uma.json @@ -0,0 +1,59 @@ +{ + "admin": { + "policies": "Manage Protected Resource Policies" + }, + "policy" : { + "resource-sets": "Resource Sets", + "edit-policies": "Edit Policies", + "new-policy": "New Policy", + "edit-policy": "Edit Policy", + "loading-policies": "Policies", + "loading-policy": "Policy", + "loading-rs": "Resource Set", + "rs-table": { + "confirm": "Are you sure you want to delete this resource set?", + "no-resource-sets": "There are no resource sets registered. Introduce a protected to this authorization server to let it register some.", + "scopes": "Scopes", + "shared-with": "Shared with:", + "shared-nobody": "NOBODY", + "shared-nobody-tooltip": "This resource is not accessible by anyone else, edit the policies and share it with someone.", + "sharing": "Sharing Policies" + }, + "policy-table": { + "new": "Add New Policy", + "return": "Return to list", + "edit": "Edit Policy", + "confirm": "Are you sure you want to delete this policy?", + "delete": "Delete", + "no-policies": "There are no policies for this resource set: This resource set is inaccessible by others.", + "required-claims": "Required Claims", + "required-claims-info": "Users that you share this resource will with need to be able to present the following claims in order to access the resource.", + "remove": "Remove", + "issuers": "Issuers", + "claim": "Claim", + "value": "Value" + }, + "policy-form": { + "email-address": "email address", + "share-email": "Share with email address", + "new": "New Policy", + "edit": "Edit Policy", + "claim-name": "claim name", + "friendly-claim-name": "friendly claim name", + "claim-value": "claim value", + "value-type-text": "Text", + "value-type-number": "Number", + "clear-all": "Clear all claims", + "clear-all-confirm": "Are you sure you want to clear all claims from this policy?" + }, + "webfinger-error": "Error", + "webfinger-error-description": "The server was unable to find an identity provider for __email__.", + "advanced-error": "Error", + "advanced-error-description": "There was an error saving your advanced claim. Did you fill in all required fields?" + }, + "sidebar": { + "personal": { + "resource_policies": "Manage Protected Resource Policies" + } + } +} \ No newline at end of file diff --git a/uma-server-webapp/src/main/webapp/resources/js/locale/zh/uma.json b/uma-server-webapp/src/main/webapp/resources/js/locale/zh/uma.json new file mode 100644 index 0000000000..e2444c4ea1 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/resources/js/locale/zh/uma.json @@ -0,0 +1,59 @@ +{ + "admin": { + "policies": "管理受保护资源的政策" + }, + "policy" : { + "resource-sets": "资源集", + "edit-policies": "编辑政策", + "new-policy": "新建政策", + "edit-policy": "编辑政策", + "loading-policies": "政策", + "loading-policy": "政策", + "loading-rs": "资源集", + "rs-table": { + "confirm": "确定要删除该资源?", + "no-resource-sets": "尚未有已注册的资源集。您可在此授权服务器中注册一个。", + "scopes": "范围", + "shared-with": "共享给:", + "shared-nobody": "不共享", + "shared-nobody-tooltip": "此资源别人无法访问,请编辑政策使其与其他人共享。", + "sharing": "共享政策" + }, + "policy-table": { + "new": "新建政策", + "return": "返回到列表", + "edit": "编辑政策", + "confirm": "确定要删除该政策?", + "delete": "删除", + "no-policies": "此资源集尚未有政策:别人无法访问此资源集。", + "required-claims": "必须的声明", + "required-claims-info": "与您共享此资源的用户必须具备以下声明,才能访问该资源。", + "remove": "移除", + "issuers": "签发者", + "claim": "声明项", + "value": "值" + }, + "policy-form": { + "email-address": "email地址", + "share-email": "连带email地址共享", + "new": "新建政策", + "edit": "编辑政策", + "claim-name": "声明项名称", + "friendly-claim-name": "声明的显示名", + "claim-value": "声明的值", + "value-type-text": "文本", + "value-type-number": "数字", + "clear-all": "清除全部声明", + "clear-all-confirm": "您是否要从此政策中清除全部声明?" + }, + "webfinger-error": "错误", + "webfinger-error-description": "服务器无法找到__email__的身份提供者。", + "advanced-error": "错误", + "advanced-error-description": "保存高级声明时出错。您是否填写了全部必填项?" + }, + "sidebar": { + "personal": { + "resource_policies": "管理受保护资源的政策" + } + } +} \ No newline at end of file diff --git a/uma-server-webapp/src/main/webapp/resources/js/locale/zh_CN/uma.json b/uma-server-webapp/src/main/webapp/resources/js/locale/zh_CN/uma.json new file mode 100644 index 0000000000..e2444c4ea1 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/resources/js/locale/zh_CN/uma.json @@ -0,0 +1,59 @@ +{ + "admin": { + "policies": "管理受保护资源的政策" + }, + "policy" : { + "resource-sets": "资源集", + "edit-policies": "编辑政策", + "new-policy": "新建政策", + "edit-policy": "编辑政策", + "loading-policies": "政策", + "loading-policy": "政策", + "loading-rs": "资源集", + "rs-table": { + "confirm": "确定要删除该资源?", + "no-resource-sets": "尚未有已注册的资源集。您可在此授权服务器中注册一个。", + "scopes": "范围", + "shared-with": "共享给:", + "shared-nobody": "不共享", + "shared-nobody-tooltip": "此资源别人无法访问,请编辑政策使其与其他人共享。", + "sharing": "共享政策" + }, + "policy-table": { + "new": "新建政策", + "return": "返回到列表", + "edit": "编辑政策", + "confirm": "确定要删除该政策?", + "delete": "删除", + "no-policies": "此资源集尚未有政策:别人无法访问此资源集。", + "required-claims": "必须的声明", + "required-claims-info": "与您共享此资源的用户必须具备以下声明,才能访问该资源。", + "remove": "移除", + "issuers": "签发者", + "claim": "声明项", + "value": "值" + }, + "policy-form": { + "email-address": "email地址", + "share-email": "连带email地址共享", + "new": "新建政策", + "edit": "编辑政策", + "claim-name": "声明项名称", + "friendly-claim-name": "声明的显示名", + "claim-value": "声明的值", + "value-type-text": "文本", + "value-type-number": "数字", + "clear-all": "清除全部声明", + "clear-all-confirm": "您是否要从此政策中清除全部声明?" + }, + "webfinger-error": "错误", + "webfinger-error-description": "服务器无法找到__email__的身份提供者。", + "advanced-error": "错误", + "advanced-error-description": "保存高级声明时出错。您是否填写了全部必填项?" + }, + "sidebar": { + "personal": { + "resource_policies": "管理受保护资源的政策" + } + } +} \ No newline at end of file diff --git a/uma-server-webapp/src/main/webapp/resources/js/locale/zh_TW/uma.json b/uma-server-webapp/src/main/webapp/resources/js/locale/zh_TW/uma.json new file mode 100644 index 0000000000..5232328326 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/resources/js/locale/zh_TW/uma.json @@ -0,0 +1,59 @@ +{ + "admin": { + "policies": "管理受保護資源的政策" + }, + "policy" : { + "resource-sets": "資源集", + "edit-policies": "編輯政策", + "new-policy": "新建政策", + "edit-policy": "編輯政策", + "loading-policies": "政策", + "loading-policy": "政策", + "loading-rs": "資源集", + "rs-table": { + "confirm": "確定要刪除該資源?", + "no-resource-sets": "尚未有已注冊的資源集。您可在此授權伺服器中注冊一個。", + "scopes": "范圍", + "shared-with": "共享給:", + "shared-nobody": "不共享", + "shared-nobody-tooltip": "此資源別人無法訪問,請編輯政策使其與其他人共享。", + "sharing": "共享政策" + }, + "policy-table": { + "new": "新建政策", + "return": "返回到列表", + "edit": "編輯政策", + "confirm": "確定要刪除該政策?", + "delete": "刪除", + "no-policies": "此資源集尚未有政策:別人無法訪問此資源集。", + "required-claims": "必須的聲明", + "required-claims-info": "與您共享此資源的用戶必須具備以下聲明,才能訪問該資源。", + "remove": "移除", + "issuers": "簽發者", + "claim": "聲明項", + "value": "值" + }, + "policy-form": { + "email-address": "email地址", + "share-email": "連帶email地址共享", + "new": "新建政策", + "edit": "編輯政策", + "claim-name": "聲明項名稱", + "friendly-claim-name": "聲明的顯示名", + "claim-value": "聲明的值", + "value-type-text": "文本", + "value-type-number": "數字", + "clear-all": "清除全部聲明", + "clear-all-confirm": "您是否要從此政策中清除全部聲明?" + }, + "webfinger-error": "錯誤", + "webfinger-error-description": "伺服器無法找到__email__的身份提供者。", + "advanced-error": "錯誤", + "advanced-error-description": "保存高級聲明時出錯。您是否填寫了全部必填項?" + }, + "sidebar": { + "personal": { + "resource_policies": "管理受保護資源的政策" + } + } +} \ No newline at end of file diff --git a/uma-server-webapp/src/main/webapp/resources/js/policy.js b/uma-server-webapp/src/main/webapp/resources/js/policy.js new file mode 100644 index 0000000000..6a3b6420c0 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/resources/js/policy.js @@ -0,0 +1,786 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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. + *******************************************************************************/ + +var ResourceSetModel = Backbone.Model.extend({ + urlRoot: 'api/resourceset' +}); + +var ResourceSetCollection = Backbone.Collection.extend({ + model: ResourceSetModel, + url: 'api/resourceset' +}); + +var PolicyModel = Backbone.Model.extend({ + urlRoot: function() { + return 'api/resourceset/' + this.options.rsid + '/policy/'; + }, + initialize: function(model, options) { + this.options = options; + } +}); + +var PolicyCollection = Backbone.Collection.extend({ + model: PolicyModel, + url: function() { + return 'api/resourceset/' + this.options.rsid + '/policy/'; + }, + initialize: function(models, options) { + this.options = options; + } +}); + +var ResourceSetListView = Backbone.View.extend({ + tagName: 'span', + + initialize:function (options) { + this.options = options; + }, + + load:function(callback) { + if (this.model.isFetched && + this.options.clientList.isFetched && + this.options.systemScopeList.isFetched) { + callback(); + return; + } + + $('#loadingbox').sheet('show'); + $('#loading').html( + '' + $.t('policy.resource-sets') + ' ' + + '' + $.t('common.clients') + ' ' + + '' + $.t('common.scopes') + ' ' + ); + + $.when(this.model.fetchIfNeeded({success:function(e) {$('#loading-resourcesets').addClass('label-success');}}), + this.options.clientList.fetchIfNeeded({success:function(e) {$('#loading-clients').addClass('label-success');}}), + this.options.systemScopeList.fetchIfNeeded({success:function(e) {$('#loading-scopes').addClass('label-success');}})) + .done(function() { + $('#loadingbox').sheet('hide'); + callback(); + }); + }, + + events: { + "click .refresh-table":"refreshTable" + }, + + render:function (eventName) { + $(this.el).html($('#tmpl-resource-set-table').html()); + + var _self = this; + + _.each(this.model.models, function (resourceSet) { + + // look up client + var client = this.options.clientList.getByClientId(resourceSet.get('clientId')); + + // if there's no client ID, this is an error! + if (client != null) { + var view = new ResourceSetView({model: resourceSet, client: client, systemScopeList: _self.options.systemScopeList}); + view.parentView = _self; + $('#resource-set-table', this.el).append(view.render().el); + } + + }, this); + + this.togglePlaceholder(); + $(this.el).i18n(); + return this; + }, + + togglePlaceholder:function() { + if (this.model.length > 0) { + $('#resource-set-table', this.el).show(); + $('#resource-set-table-empty', this.el).hide(); + } else { + $('#resource-set-table', this.el).hide(); + $('#resource-set-table-empty', this.el).show(); + } + }, + + refreshTable:function(e) { + e.preventDefault(); + var _self = this; + $('#loadingbox').sheet('show'); + $('#loading').html( + '' + $.t('policy.resource-sets') + ' ' + + '' + $.t('common.clients') + ' ' + + '' + $.t('common.scopes') + ' ' + ); + + $.when(this.model.fetch({success:function(e) {$('#loading-resourcesets').addClass('label-success');}}), + this.options.clientList.fetch({success:function(e) {$('#loading-clients').addClass('label-success');}}), + this.options.systemScopeList.fetch({success:function(e) {$('#loading-scopes').addClass('label-success');}})) + .done(function() { + $('#loadingbox').sheet('hide'); + _self.render(); + }); + } + + +}); + + +var ResourceSetView = Backbone.View.extend({ + tagName: 'tr', + + initialize:function(options) { + this.options = options; + if (!this.template) { + this.template = _.template($('#tmpl-resource-set').html()); + } + + if (!this.scopeTemplate) { + this.scopeTemplate = _.template($('#tmpl-scope-list').html()); + } + + if (!this.moreInfoTemplate) { + this.moreInfoTemplate = _.template($('#tmpl-client-more-info-block').html()); + } + + this.model.bind('change', this.render, this); + }, + + render:function(eventName) { + + var json = {rs: this.model.toJSON(), client: this.options.client.toJSON()}; + + this.$el.html(this.template(json)); + + $('.scope-list', this.el).html(this.scopeTemplate({scopes: this.model.get('scopes'), systemScopes: this.options.systemScopeList})); + + $('.client-more-info-block', this.el).html(this.moreInfoTemplate({client: this.options.client.toJSON()})); + + $(this.el).i18n(); + return this; + }, + + events:{ + 'click .btn-edit': 'editPolicies', + 'click .btn-delete': 'deleteResourceSet', + 'click .toggleMoreInformation': 'toggleMoreInformation' + }, + + editPolicies:function(e) { + e.preventDefault(); + app.navigate('user/policy/' + this.model.get('id'), {trigger: true}); + }, + + deleteResourceSet:function(e) { + e.preventDefault(); + + if (confirm($.t('policy.rs-table.confirm'))) { + var _self = this; + + this.model.destroy({ + dataType: false, processData: false, + success:function () { + _self.$el.fadeTo("fast", 0.00, function () { //fade + $(this).slideUp("fast", function () { //slide up + $(this).remove(); //then remove from the DOM + _self.parentView.togglePlaceholder(); + }); + }); + }, + error:function (error, response) { + console.log("An error occurred when deleting a resource set"); + + //Pull out the response text. + var responseJson = JSON.parse(response.responseText); + + //Display an alert with an error message + $('#modalAlert div.modal-header').html(responseJson.error); + $('#modalAlert div.modal-body').html(responseJson.error_description); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + } + }); + + _self.parentView.delegateEvents(); + } + + return false; + + }, + + toggleMoreInformation:function(e) { + e.preventDefault(); + if ($('.moreInformation', this.el).is(':visible')) { + // hide it + $('.moreInformation', this.el).hide('fast'); + $('.toggleMoreInformation i', this.el).attr('class', 'icon-chevron-right'); + $('.moreInformationContainer', this.el).removeClass('alert').removeClass('alert-info').addClass('muted'); + + } else { + // show it + $('.moreInformation', this.el).show('fast'); + $('.toggleMoreInformation i', this.el).attr('class', 'icon-chevron-down'); + $('.moreInformationContainer', this.el).addClass('alert').addClass('alert-info').removeClass('muted'); + } + }, + +}); + +var PolicyListView = Backbone.View.extend({ + tagName: 'span', + + initialize:function(options) { + this.options = options; + }, + + load:function(callback) { + if (this.model.isFetched && + this.options.rs.isFetched && + this.options.systemScopeList.isFetched) { + callback(); + return; + } + + $('#loadingbox').sheet('show'); + $('#loading').html( + '' + $.t('policy.loading-policies') + ' ' + + '' + $.t('policy.loading-rs') + ' ' + + '' + $.t("common.scopes") + ' ' + ); + + $.when(this.model.fetchIfNeeded({success:function(e) {$('#loading-policies').addClass('label-success');}}), + this.options.rs.fetchIfNeeded({success:function(e) {$('#loading-rs').addClass('label-success');}}), + this.options.systemScopeList.fetchIfNeeded({success:function(e) {$('#loading-scopes').addClass('label-success');}})) + .done(function() { + $('#loadingbox').sheet('hide'); + callback(); + }); + }, + + events:{ + 'click .btn-add':'addPolicy', + 'click .btn-cancel':'cancel' + }, + + cancel:function(e) { + e.preventDefault(); + app.navigate('user/policy', {trigger: true}); + }, + + togglePlaceholder:function() { + if (this.model.length > 0) { + $('#policy-info', this.el).show(); + $('#policy-table', this.el).show(); + $('#policy-table-empty', this.el).hide(); + } else { + $('#policy-info', this.el).hide(); + $('#policy-table', this.el).hide(); + $('#policy-table-empty', this.el).show(); + } + }, + + addPolicy:function(e) { + e.preventDefault(); + app.navigate('user/policy/' + this.options.rs.get('id') +'/new', {trigger: true}); + }, + + render:function (eventName) { + $(this.el).html($('#tmpl-policy-table').html()); + + var _self = this; + + _.each(this.model.models, function (policy) { + + var view = new PolicyView({model: policy, systemScopeList: _self.options.systemScopeList, rs: _self.options.rs}); + view.parentView = _self; + $('#policy-table', this.el).append(view.render().el); + + }, this); + + this.togglePlaceholder(); + $(this.el).i18n(); + return this; + } +}); + + +var PolicyView = Backbone.View.extend({ + tagName: 'tr', + + initialize:function(options) { + this.options = options; + + if (!this.template) { + this.template = _.template($('#tmpl-policy').html()); + } + + if (!this.scopeTemplate) { + this.scopeTemplate = _.template($('#tmpl-scope-list').html()); + } + + + }, + + events:{ + 'click .btn-edit':'editPolicy', + 'click .btn-remove':'removePolicy' + }, + + editPolicy:function(e) { + e.preventDefault(); + app.navigate('user/policy/' + this.options.rs.get("id") + '/' + this.model.get('id'), {trigger: true}); + }, + + removePolicy:function(e) { + e.preventDefault(); + + if (confirm($.t('policy.policy-table.confirm'))) { + var _self = this; + this.model.destroy({ + dataType: false, processData: false, + success:function () { + _self.$el.fadeTo("fast", 0.00, function () { //fade + $(this).slideUp("fast", function () { //slide up + $(this).remove(); //then remove from the DOM + _self.parentView.togglePlaceholder(); + }); + }); + }, + error:function (error, response) { + console.log("An error occurred when deleting a client"); + + //Pull out the response text. + var responseJson = JSON.parse(response.responseText); + + //Display an alert with an error message + $('#modalAlert div.modal-header').html(responseJson.error); + $('#modalAlert div.modal-body').html(responseJson.error_description); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + } + }); + + _self.parentView.delegateEvents(); + } + }, + + render:function (eventName) { + var json = this.model.toJSON(); + + this.$el.html(this.template(json)); + + $('.scope-list', this.el).html(this.scopeTemplate({scopes: this.model.get('scopes'), systemScopes: this.options.systemScopeList})); + + $(this.el).i18n(); + return this; + } + + +}); + + +var PolicyFormView = Backbone.View.extend({ + tagName: 'div', + + initialize:function(options) { + this.options = options; + + if (!this.template) { + this.template = _.template($('#tmpl-policy-form').html()); + } + + this.issuerCollection = new Backbone.Collection(); + + }, + + events:{ + 'click .btn-share': 'addWebfingerClaim', + 'click .btn-share-advanced': 'addAdvancedClaim', + 'click .btn-clear': 'clearAllClaims', + 'click .btn-save': 'savePolicy', + 'click .btn-cancel': 'cancel' + }, + + load:function(callback) { + if (this.model.isFetched && + this.options.rs.isFetched && + this.options.systemScopeList.isFetched) { + callback(); + return; + } + + $('#loadingbox').sheet('show'); + $('#loading').html( + '' + $.t('policy.loading-policy') + ' ' + + '' + $.t('policy.loading-rs') + ' ' + + '' + $.t("common.scopes") + ' ' + ); + + $.when(this.model.fetchIfNeeded({success:function(e) {$('#loading-policies').addClass('label-success');}}), + this.options.rs.fetchIfNeeded({success:function(e) {$('#loading-rs').addClass('label-success');}}), + this.options.systemScopeList.fetchIfNeeded({success:function(e) {$('#loading-scopes').addClass('label-success');}})) + .done(function() { + $('#loadingbox').sheet('hide'); + callback(); + }); + }, + + addWebfingerClaim:function(e) { + e.preventDefault(); + + // post to the webfinger helper and get the response back + + var _self = this; + + var email = $('#email', this.el).val(); + + $('#loadingbox').sheet('show'); + $('#loading').html( + 'Looking up identity provider...' + ); + + var base = $('base').attr('href'); + $.getJSON(base + '/api/emailsearch?' + $.param({'identifier': email}), function(data) { + + // grab the current state of the scopes checkboxes just in case + var scopes = $('#scopes input[type="checkbox"]:checked').map(function(idx, elem) { return $(elem).val(); }).get(); + + _self.model.set({ + scopes: scopes, + claimsRequired: data + }, {trigger: false}); + + _self.render(); + + $('#loadingbox').sheet('hide'); + + }).error(function(jqXHR, textStatus, errorThrown) { + console.log("An error occurred when doing a webfinger lookup", errorThrown); + + $('#loadingbox').sheet('hide'); + + //Display an alert with an error message + $('#modalAlert div.modal-header').html($.t('policy.webfinger-error')); + $('#modalAlert div.modal-body').html($.t('policy.webfinger-error-description', {email: email})); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + }); + + }, + + addAdvancedClaim:function(e) { + e.preventDefault(); + + var name = $('#name', this.el).val(); + var friendly = $('#friendly-name', this.el).val(); + var rawValue = $('#value', this.el).val(); + var valueType = $('#value-type', this.el).val(); + var value = null; + if (valueType == 'number') { + value = Number(rawValue); + } else if (valueType == 'boolean') { + value = (rawValue.toLowerCase() == 'true'); + } else if (valueType == 'json') { + value = JSON.parse(rawValue); + } else { + // treat it as a string, the default + value = rawValue; + } + + var issuers = this.issuerCollection.pluck('item'); + + console.log(name, friendly, rawValue, valueType, value, issuers); + + if (!_.isEmpty(issuers) + && name + && value) { + // we've got a valid claim, add it to our set + // grab the current state of the scopes checkboxes just in case + var scopes = $('#scopes input[type="checkbox"]:checked').map(function(idx, elem) { return $(elem).val(); }).get(); + + var claimsRequired = this.model.get('claimsRequired'); + if (!claimsRequired) { + claimsRequired = []; + } + claimsRequired.push({ + name: name, + friendlyName: friendly, + value: value, + issuer: issuers + }); + + this.model.set({ + scopes: scopes, + claimsRequired: claimsRequired + }, {trigger: false}); + + $('#name', this.el).val(''); + $('#friendly-name', this.el).val(''); + $('#value', this.el).val(''); + $('#value-type', this.el).val('text'); + + this.render(); + + // re-select the advanced tab + $('a[data-target="#policy-advanced-tab"]', this.el).tab('show') + + } else { + // something is missing + $('#loadingbox').sheet('hide'); + + //Display an alert with an error message + $('#modalAlert div.modal-header').html($.t('policy.advanced-error')); + $('#modalAlert div.modal-body').html($.t('policy.advanced-error-description')); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + } + }, + + clearAllClaims:function(e) { + e.preventDefault(); + + if (confirm($.t('policy.policy-form.clear-all-confirm'))) { + + var scopes = $('#scopes input[type="checkbox"]:checked').map(function(idx, elem) { return $(elem).val(); }).get(); + + var claimsRequired = []; + + this.model.set({ + scopes: scopes, + claimsRequired: claimsRequired + }, {trigger: false}); + + this.render(); + } + }, + + savePolicy:function(e) { + e.preventDefault(); + + // get all the scopes that are checked + var scopes = $('#scopes input[type="checkbox"]:checked').map(function(idx, elem) { return $(elem).val(); }).get(); + + var valid = this.model.set({ + scopes: scopes + }); + + if (valid) { + + var _self = this; + this.model.save({}, { + success:function() { + app.systemScopeList.add(_self.model); + + // refresh the associated RS + _self.options.rs.fetch({success: function() { + app.navigate('user/policy/' + _self.options.rs.get('id'), {trigger: true}); + }}); + + }, + error:function(error, response) { + + //Pull out the response text. + var responseJson = JSON.parse(response.responseText); + + //Display an alert with an error message + $('#modalAlert div.modal-header').html(responseJson.error); + $('#modalAlert div.modal-body').html(responseJson.error_description); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + } + }); + } + + return false; + + }, + + cancel:function(e) { + e.preventDefault(); + app.navigate('user/policy/' + this.options.rs.get('id'), {trigger: true}); + }, + + render:function (eventName) { + var json = this.model.toJSON(); + var rs = this.options.rs.toJSON(); + + this.$el.html(this.template({policy: json, rs: rs})); + + // build and bind issuer view + var issuerView = new ListWidgetView({ + placeholder: $.t('policy.policy-form.issuer-placeholder'), + helpBlockText: $.t('policy.policy-form.issuer-help'), + collection: this.issuerCollection}); + $("#issuers .controls",this.el).html(issuerView.render().el); + + $(this.el).i18n(); + + return this; + } +}); + + +ui.routes.push({path: "user/policy", name: "policy", callback: + function() { + + this.breadCrumbView.collection.reset(); + this.breadCrumbView.collection.add([ + {text:$.t('admin.home'), href:""}, + {text:$.t('policy.resource-sets'), href:"manage/#user/policy"} + ]); + + this.updateSidebar('user/policy'); + + var view = new ResourceSetListView({model: this.resourceSetList, clientList: this.clientList, systemScopeList: this.systemScopeList}); + + view.load(function() { + $('#content').html(view.render().el); + setPageTitle($.t('policy.resource-sets')); + }); + + } +}); + +ui.routes.push({path: "user/policy/:rsid", name: "editPolicies", callback: + function(rsid) { + + this.breadCrumbView.collection.reset(); + this.breadCrumbView.collection.add([ + {text:$.t('admin.home'), href:""}, + {text:$.t('policy.resource-sets'), href:"manage/#user/policy"}, + {text:$.t('policy.edit-policies'), href:"manage/#user/policy/" + rsid} + ]); + + this.updateSidebar('user/policy'); + + var rs = this.resourceSetList.get(rsid); + var policies = null; + if (rs == null) { + // need to load it directly + policies = new PolicyCollection([], {rsid: rsid}); + rs = new ResourceSetModel({id: rsid}); + this.resourceSetList.add(rs); // it will be loaded below, don't need to load it again in the future + } else { + // the resource set is loaded, preload the claims + policies = new PolicyCollection(rs.get('policies'), {rsid: rsid}); + policies.isFetched = true; + } + + var view = new PolicyListView({model: policies, rs: rs, systemScopeList: this.systemScopeList}); + + view.load(function() { + $('#content').html(view.render().el); + setPageTitle($.t('policy.edit-policy')); + }); + + } +}); + +ui.routes.push({path: "user/policy/:rsid/new", name: "newPolicy", callback: + function(rsid) { + + this.breadCrumbView.collection.reset(); + this.breadCrumbView.collection.add([ + {text:$.t('admin.home'), href:""}, + {text:$.t('policy.resource-sets'), href:"manage/#user/policy"}, + {text:$.t('policy.edit-policies'), href:"manage/#user/policy/" + rsid}, + {text:$.t('policy.new-policy'), href:"manage/#user/policy/" + rsid + "/new"} + ]); + + this.updateSidebar('user/policy'); + + var policy = policy = new PolicyModel({}, {rsid: rsid}); + + var rs = this.resourceSetList.get(rsid); + if (rs == null) { + // need to load it directly + rs = new ResourceSetModel({id: rsid}); + this.resourceSetList.add(rs); // it will be loaded below, don't need to load it again in the future + } + + var view = new PolicyFormView({model: policy, rs: rs, systemScopeList: this.systemScopeList}); + + view.load(function() { + $('#content').html(view.render().el); + setPageTitle($.t('policy.edit-policy')); + }); + } +}); + +ui.routes.push({path: "user/policy/:rsid/:pid", name: "editPolicy", callback: + function(rsid, pid) { + this.breadCrumbView.collection.reset(); + this.breadCrumbView.collection.add([ + {text:$.t('admin.home'), href:""}, + {text:$.t('policy.resource-sets'), href:"manage/#user/policy"}, + {text:$.t('policy.edit-policies'), href:"manage/#user/policy/" + rsid}, + {text:$.t('policy.edit-policy'), href:"manage/#user/policy/" + rsid + "/" + pid} + ]); + + this.updateSidebar('user/policy'); + + var rs = this.resourceSetList.get(rsid); + var policy = null; + if (rs == null) { + // need to load it directly + policy = new PolicyModel({id: pid}, {rsid: rsid}); + rs = new ResourceSetModel({id: rsid}); + this.resourceSetList.add(rs); // it will be loaded below, don't need to load it again in the future + } else { + // the resource set is loaded, preload the claims + _.each(rs.get('policies'), function(p) { + if (p.id == pid) { + policy = new PolicyModel(p, {rsid: rsid}); + policy.isFetched = true; + } + }); + if (policy == null) { + // need to load it directly + policy = new PolicyModel({id: pid}, {rsid: rsid}); + } + } + + var view = new PolicyFormView({model: policy, rs: rs, systemScopeList: this.systemScopeList}); + + view.load(function() { + $('#content').html(view.render().el); + setPageTitle($.t('policy.edit-policy')); + }); + + + } +}); + +ui.templates.push('resources/template/policy.html'); + +ui.init.push(function(app) { + app.resourceSetList = new ResourceSetCollection(); +}); diff --git a/uma-server-webapp/src/main/webapp/resources/template/policy.html b/uma-server-webapp/src/main/webapp/resources/template/policy.html new file mode 100644 index 0000000000..576da1b1ad --- /dev/null +++ b/uma-server-webapp/src/main/webapp/resources/template/policy.html @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uma-server/pom.xml b/uma-server/pom.xml new file mode 100644 index 0000000000..515c7200f5 --- /dev/null +++ b/uma-server/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + org.mitre + openid-connect-parent + 1.3.5-SNAPSHOT + .. + + uma-server + UMA Server Library + User Managed Access (UMA) extension of the MITREid Connect server + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java-version} + ${java-version} + + + + + + + org.mitre + openid-connect-server + + + org.mitre + openid-connect-client + + + diff --git a/uma-server/src/main/java/org/mitre/uma/repository/impl/JpaPermissionRepository.java b/uma-server/src/main/java/org/mitre/uma/repository/impl/JpaPermissionRepository.java new file mode 100644 index 0000000000..6f460d200d --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/repository/impl/JpaPermissionRepository.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.repository.impl; + +import java.util.Collection; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; + +import org.mitre.uma.model.Permission; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.repository.PermissionRepository; +import org.mitre.util.jpa.JpaUtil; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author jricher + * + */ +@Repository +public class JpaPermissionRepository implements PermissionRepository { + + @PersistenceContext(unitName="defaultPersistenceUnit") + private EntityManager em; + + @Override + @Transactional(value="defaultTransactionManager") + public PermissionTicket save(PermissionTicket p) { + return JpaUtil.saveOrUpdate(p.getId(), em, p); + } + + /* (non-Javadoc) + * @see org.mitre.uma.repository.PermissionRepository#getByTicket(java.lang.String) + */ + @Override + public PermissionTicket getByTicket(String ticket) { + TypedQuery query = em.createNamedQuery(PermissionTicket.QUERY_TICKET, PermissionTicket.class); + query.setParameter(PermissionTicket.PARAM_TICKET, ticket); + return JpaUtil.getSingleResult(query.getResultList()); + } + + /* (non-Javadoc) + * @see org.mitre.uma.repository.PermissionRepository#getAll() + */ + @Override + public Collection getAll() { + TypedQuery query = em.createNamedQuery(PermissionTicket.QUERY_ALL, PermissionTicket.class); + return query.getResultList(); + } + + /* (non-Javadoc) + * @see org.mitre.uma.repository.PermissionRepository#saveRawPermission(org.mitre.uma.model.Permission) + */ + @Override + @Transactional(value="defaultTransactionManager") + public Permission saveRawPermission(Permission p) { + return JpaUtil.saveOrUpdate(p.getId(), em, p); + } + + /* (non-Javadoc) + * @see org.mitre.uma.repository.PermissionRepository#getById(java.lang.Long) + */ + @Override + public Permission getById(Long permissionId) { + return em.find(Permission.class, permissionId); + } + + /* (non-Javadoc) + * @see org.mitre.uma.repository.PermissionRepository#getPermissionTicketsForResourceSet(org.mitre.uma.model.ResourceSet) + */ + @Override + public Collection getPermissionTicketsForResourceSet(ResourceSet rs) { + TypedQuery query = em.createNamedQuery(PermissionTicket.QUERY_BY_RESOURCE_SET, PermissionTicket.class); + query.setParameter(PermissionTicket.PARAM_RESOURCE_SET_ID, rs.getId()); + return query.getResultList(); + } + + /* (non-Javadoc) + * @see org.mitre.uma.repository.PermissionRepository#remove(org.mitre.uma.model.PermissionTicket) + */ + @Override + @Transactional(value="defaultTransactionManager") + public void remove(PermissionTicket ticket) { + PermissionTicket found = getByTicket(ticket.getTicket()); + if (found != null) { + em.remove(found); + } + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/repository/impl/JpaResourceSetRepository.java b/uma-server/src/main/java/org/mitre/uma/repository/impl/JpaResourceSetRepository.java new file mode 100644 index 0000000000..e19236c399 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/repository/impl/JpaResourceSetRepository.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.repository.impl; + +import java.util.Collection; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; + +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.repository.ResourceSetRepository; +import org.mitre.util.jpa.JpaUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author jricher + * + */ +@Repository +public class JpaResourceSetRepository implements ResourceSetRepository { + + @PersistenceContext(unitName="defaultPersistenceUnit") + private EntityManager em; + private static Logger logger = LoggerFactory.getLogger(JpaResourceSetRepository.class); + + @Override + @Transactional(value="defaultTransactionManager") + public ResourceSet save(ResourceSet rs) { + return JpaUtil.saveOrUpdate(rs.getId(), em, rs); + } + + @Override + public ResourceSet getById(Long id) { + return em.find(ResourceSet.class, id); + } + + @Override + @Transactional(value="defaultTransactionManager") + public void remove(ResourceSet rs) { + ResourceSet found = getById(rs.getId()); + if (found != null) { + em.remove(found); + } else { + logger.info("Tried to remove unknown resource set: " + rs.getId()); + } + } + + @Override + public Collection getAllForOwner(String owner) { + TypedQuery query = em.createNamedQuery(ResourceSet.QUERY_BY_OWNER, ResourceSet.class); + query.setParameter(ResourceSet.PARAM_OWNER, owner); + return query.getResultList(); + } + + @Override + public Collection getAllForOwnerAndClient(String owner, String clientId) { + TypedQuery query = em.createNamedQuery(ResourceSet.QUERY_BY_OWNER_AND_CLIENT, ResourceSet.class); + query.setParameter(ResourceSet.PARAM_OWNER, owner); + query.setParameter(ResourceSet.PARAM_CLIENTID, clientId); + return query.getResultList(); + } + + @Override + public Collection getAll() { + TypedQuery query = em.createNamedQuery(ResourceSet.QUERY_ALL, ResourceSet.class); + return query.getResultList(); + } + + /* (non-Javadoc) + * @see org.mitre.uma.repository.ResourceSetRepository#getAllForClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public Collection getAllForClient(String clientId) { + TypedQuery query = em.createNamedQuery(ResourceSet.QUERY_BY_CLIENT, ResourceSet.class); + query.setParameter(ResourceSet.PARAM_CLIENTID, clientId); + return query.getResultList(); + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultPermissionService.java b/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultPermissionService.java new file mode 100644 index 0000000000..8b9c379e48 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultPermissionService.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import java.sql.Date; +import java.util.Set; +import java.util.UUID; + +import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.uma.model.Permission; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.repository.PermissionRepository; +import org.mitre.uma.service.PermissionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException; +import org.springframework.stereotype.Service; + +/** + * @author jricher + * + */ +@Service +public class DefaultPermissionService implements PermissionService { + + @Autowired + private PermissionRepository repository; + + @Autowired + private SystemScopeService scopeService; + + private Long permissionExpirationSeconds = 60L * 60L; // 1 hr + + /* (non-Javadoc) + * @see org.mitre.uma.service.PermissionService#create(org.mitre.uma.model.ResourceSet, java.util.Set) + */ + @Override + public PermissionTicket createTicket(ResourceSet resourceSet, Set scopes) { + + // check to ensure that the scopes requested are a subset of those in the resource set + + if (!scopeService.scopesMatch(resourceSet.getScopes(), scopes)) { + throw new InsufficientScopeException("Scopes of resource set are not enough for requested permission."); + } + + Permission perm = new Permission(); + perm.setResourceSet(resourceSet); + perm.setScopes(scopes); + + PermissionTicket ticket = new PermissionTicket(); + ticket.setPermission(perm); + ticket.setTicket(UUID.randomUUID().toString()); + ticket.setExpiration(new Date(System.currentTimeMillis() + permissionExpirationSeconds * 1000L)); + + return repository.save(ticket); + + } + + /* (non-Javadoc) + * @see org.mitre.uma.service.PermissionService#getByTicket(java.lang.String) + */ + @Override + public PermissionTicket getByTicket(String ticket) { + return repository.getByTicket(ticket); + } + + /* (non-Javadoc) + * @see org.mitre.uma.service.PermissionService#updateTicket(org.mitre.uma.model.PermissionTicket) + */ + @Override + public PermissionTicket updateTicket(PermissionTicket ticket) { + if (ticket.getId() != null) { + return repository.save(ticket); + } else { + return null; + } + + } + + + +} diff --git a/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultResourceSetService.java b/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultResourceSetService.java new file mode 100644 index 0000000000..a5c3e5ec4b --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultResourceSetService.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import java.util.Collection; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.Policy; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.repository.PermissionRepository; +import org.mitre.uma.repository.ResourceSetRepository; +import org.mitre.uma.service.ResourceSetService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +/** + * @author jricher + * + */ +@Service +@Primary +public class DefaultResourceSetService implements ResourceSetService { + + private static final Logger logger = LoggerFactory.getLogger(DefaultResourceSetService.class); + + @Autowired + private ResourceSetRepository repository; + + @Autowired + private OAuth2TokenRepository tokenRepository; + + @Autowired + private PermissionRepository ticketRepository; + + @Override + public ResourceSet saveNew(ResourceSet rs) { + + if (rs.getId() != null) { + throw new IllegalArgumentException("Can't save a new resource set with an ID already set to it."); + } + + if (!checkScopeConsistency(rs)) { + throw new IllegalArgumentException("Can't save a resource set with inconsistent claims."); + } + + ResourceSet saved = repository.save(rs); + + return saved; + + } + + @Override + public ResourceSet getById(Long id) { + return repository.getById(id); + } + + @Override + public ResourceSet update(ResourceSet oldRs, ResourceSet newRs) { + + if (oldRs.getId() == null || newRs.getId() == null + || !oldRs.getId().equals(newRs.getId())) { + + throw new IllegalArgumentException("Resource set IDs mismatched"); + + } + + if (!checkScopeConsistency(newRs)) { + throw new IllegalArgumentException("Can't save a resource set with inconsistent claims."); + } + + newRs.setOwner(oldRs.getOwner()); // preserve the owner tag across updates + newRs.setClientId(oldRs.getClientId()); // preserve the client id across updates + + ResourceSet saved = repository.save(newRs); + + return saved; + + } + + @Override + public void remove(ResourceSet rs) { + // find all the access tokens issued against this resource set and revoke them + Collection tokens = tokenRepository.getAccessTokensForResourceSet(rs); + for (OAuth2AccessTokenEntity token : tokens) { + tokenRepository.removeAccessToken(token); + } + + // find all outstanding tickets issued against this resource set and revoke them too + Collection tickets = ticketRepository.getPermissionTicketsForResourceSet(rs); + for (PermissionTicket ticket : tickets) { + ticketRepository.remove(ticket); + } + + repository.remove(rs); + } + + @Override + public Collection getAllForOwner(String owner) { + return repository.getAllForOwner(owner); + } + + @Override + public Collection getAllForOwnerAndClient(String owner, String clientId) { + return repository.getAllForOwnerAndClient(owner, clientId); + } + + private boolean checkScopeConsistency(ResourceSet rs) { + if (rs.getPolicies() == null) { + // nothing to check, no problem! + return true; + } + for (Policy policy : rs.getPolicies()) { + if (!rs.getScopes().containsAll(policy.getScopes())) { + return false; + } + } + // we've checked everything, we're good + return true; + } + + /* (non-Javadoc) + * @see org.mitre.uma.service.ResourceSetService#getAllForClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public Collection getAllForClient(ClientDetailsEntity client) { + return repository.getAllForClient(client.getClientId()); + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultUmaTokenService.java b/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultUmaTokenService.java new file mode 100644 index 0000000000..62bd24eac0 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/service/impl/DefaultUmaTokenService.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.repository.AuthenticationHolderRepository; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.uma.model.Permission; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.Policy; +import org.mitre.uma.service.UmaTokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Service; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +/** + * @author jricher + * + */ +@Service("defaultUmaTokenService") +public class DefaultUmaTokenService implements UmaTokenService { + + @Autowired + private AuthenticationHolderRepository authenticationHolderRepository; + + @Autowired + private OAuth2TokenEntityService tokenService; + + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private ConfigurationPropertiesBean config; + + @Autowired + private JWTSigningAndValidationService jwtService; + + + @Override + public OAuth2AccessTokenEntity createRequestingPartyToken(OAuth2Authentication o2auth, PermissionTicket ticket, Policy policy) { + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); + authHolder.setAuthentication(o2auth); + authHolder = authenticationHolderRepository.save(authHolder); + + token.setAuthenticationHolder(authHolder); + + ClientDetailsEntity client = clientService.loadClientByClientId(o2auth.getOAuth2Request().getClientId()); + token.setClient(client); + + Set ticketScopes = ticket.getPermission().getScopes(); + Set policyScopes = policy.getScopes(); + + Permission perm = new Permission(); + perm.setResourceSet(ticket.getPermission().getResourceSet()); + perm.setScopes(new HashSet<>(Sets.intersection(ticketScopes, policyScopes))); + + token.setPermissions(Sets.newHashSet(perm)); + + JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder(); + + claims.audience(Lists.newArrayList(ticket.getPermission().getResourceSet().getId().toString())); + claims.issuer(config.getIssuer()); + claims.jwtID(UUID.randomUUID().toString()); + + if (config.getRqpTokenLifeTime() != null) { + Date exp = new Date(System.currentTimeMillis() + config.getRqpTokenLifeTime() * 1000L); + + claims.expirationTime(exp); + token.setExpiration(exp); + } + + + JWSAlgorithm signingAlgorithm = jwtService.getDefaultSigningAlgorithm(); + JWSHeader header = new JWSHeader(signingAlgorithm, null, null, null, null, null, null, null, null, null, + jwtService.getDefaultSignerKeyId(), + null, null); + SignedJWT signed = new SignedJWT(header, claims.build()); + + jwtService.signJwt(signed); + + token.setJwt(signed); + + tokenService.saveAccessToken(token); + + return token; + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/service/impl/JpaRegisteredClientService.java b/uma-server/src/main/java/org/mitre/uma/service/impl/JpaRegisteredClientService.java new file mode 100644 index 0000000000..8ceb548e8c --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/service/impl/JpaRegisteredClientService.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import java.util.Collection; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; + +import org.mitre.oauth2.model.RegisteredClient; +import org.mitre.openid.connect.client.service.RegisteredClientService; +import org.mitre.uma.model.SavedRegisteredClient; +import org.mitre.uma.service.SavedRegisteredClientService; +import org.mitre.util.jpa.JpaUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author jricher + * + */ +@Service +public class JpaRegisteredClientService implements RegisteredClientService, SavedRegisteredClientService{ + + @PersistenceContext(unitName="defaultPersistenceUnit") + private EntityManager em; + + /* (non-Javadoc) + * @see org.mitre.openid.connect.client.service.RegisteredClientService#getByIssuer(java.lang.String) + */ + @Override + public RegisteredClient getByIssuer(String issuer) { + SavedRegisteredClient saved = getSavedRegisteredClientFromStorage(issuer); + + if (saved == null) { + return null; + } else { + return saved.getRegisteredClient(); + } + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.client.service.RegisteredClientService#save(java.lang.String, org.mitre.oauth2.model.RegisteredClient) + */ + @Override + @Transactional(value="defaultTransactionManager") + public void save(String issuer, RegisteredClient client) { + + + SavedRegisteredClient saved = getSavedRegisteredClientFromStorage(issuer); + + if (saved == null) { + saved = new SavedRegisteredClient(); + saved.setIssuer(issuer); + } + + saved.setRegisteredClient(client); + + em.persist(saved); + + } + + private SavedRegisteredClient getSavedRegisteredClientFromStorage(String issuer) { + TypedQuery query = em.createQuery("SELECT c from SavedRegisteredClient c where c.issuer = :issuer", SavedRegisteredClient.class); + query.setParameter("issuer", issuer); + + SavedRegisteredClient saved = JpaUtil.getSingleResult(query.getResultList()); + return saved; + } + + /** + * @return + */ + @Override + public Collection getAll() { + TypedQuery query = em.createQuery("SELECT c from SavedRegisteredClient c", SavedRegisteredClient.class); + return query.getResultList(); + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/service/impl/MatchAllClaimsOnAnyPolicy.java b/uma-server/src/main/java/org/mitre/uma/service/impl/MatchAllClaimsOnAnyPolicy.java new file mode 100644 index 0000000000..7d480bf619 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/service/impl/MatchAllClaimsOnAnyPolicy.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import java.util.Collection; +import java.util.HashSet; + +import org.mitre.uma.model.Claim; +import org.mitre.uma.model.ClaimProcessingResult; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.Policy; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ClaimsProcessingService; +import org.springframework.stereotype.Service; + +/** + * Tests if all the claims in the required set have a matching + * value in the supplied set. + * + * @author jricher + * + */ +@Service("matchAllClaimsOnAnyPolicy") +public class MatchAllClaimsOnAnyPolicy implements ClaimsProcessingService { + + /* (non-Javadoc) + * @see org.mitre.uma.service.ClaimsProcessingService#claimsAreSatisfied(java.util.Collection, java.util.Collection) + */ + @Override + public ClaimProcessingResult claimsAreSatisfied(ResourceSet rs, PermissionTicket ticket) { + Collection allUnmatched = new HashSet<>(); + for (Policy policy : rs.getPolicies()) { + Collection unmatched = checkIndividualClaims(policy.getClaimsRequired(), ticket.getClaimsSupplied()); + if (unmatched.isEmpty()) { + // we found something that's satisfied the claims, let's go with it! + return new ClaimProcessingResult(policy); + } else { + // otherwise add it to the stack to send back + allUnmatched.addAll(unmatched); + } + } + + // otherwise, tell the caller that we'll need some set of these fulfilled somehow + return new ClaimProcessingResult(allUnmatched); + } + + private Collection checkIndividualClaims(Collection claimsRequired, Collection claimsSupplied) { + + Collection claimsUnmatched = new HashSet<>(claimsRequired); + + // see if each of the required claims has a counterpart in the supplied claims set + for (Claim required : claimsRequired) { + for (Claim supplied : claimsSupplied) { + + if (required.getIssuer().containsAll(supplied.getIssuer())) { + // it's from the right issuer + + if (required.getName().equals(supplied.getName()) && + required.getValue().equals(supplied.getValue())) { + + // the claim matched, pull it from the set + claimsUnmatched.remove(required); + + } + + } + } + } + + // if there's anything left then the claims aren't satisfied, return the leftovers + return claimsUnmatched; + + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/service/impl/UmaDataServiceExtension_1_3.java b/uma-server/src/main/java/org/mitre/uma/service/impl/UmaDataServiceExtension_1_3.java new file mode 100644 index 0000000000..6e9fba180f --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/service/impl/UmaDataServiceExtension_1_3.java @@ -0,0 +1,715 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import static org.mitre.util.JsonUtils.readSet; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.RegisteredClient; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor; +import org.mitre.openid.connect.service.MITREidDataService; +import org.mitre.openid.connect.service.MITREidDataServiceExtension; +import org.mitre.openid.connect.service.MITREidDataServiceMaps; +import org.mitre.openid.connect.service.impl.MITREidDataServiceSupport; +import org.mitre.uma.model.Claim; +import org.mitre.uma.model.Permission; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.Policy; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.model.SavedRegisteredClient; +import org.mitre.uma.repository.PermissionRepository; +import org.mitre.uma.repository.ResourceSetRepository; +import org.mitre.uma.service.SavedRegisteredClientService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * @author jricher + * + */ +@Service("umaDataExtension_1_3") +public class UmaDataServiceExtension_1_3 extends MITREidDataServiceSupport implements MITREidDataServiceExtension { + + private static final String THIS_VERSION = MITREidDataService.MITREID_CONNECT_1_3; + + private static final String REGISTERED_CLIENT = "registeredClient"; + private static final String URI = "uri"; + private static final String NAME = "name"; + private static final String TYPE = "type"; + private static final String VALUE = "value"; + private static final String CLIENT_ID = "clientId"; + private static final String EXPIRATION = "expiration"; + private static final String ID = "id"; + private static final String ICON_URI = "iconUri"; + private static final String OWNER = "owner"; + private static final String POLICIES = "policies"; + private static final String SCOPES = "scopes"; + private static final String CLAIMS_REQUIRED = "claimsRequired"; + private static final String ISSUER = "issuer"; + private static final String CLAIM_TOKEN_FORMAT = "claimTokenFormat"; + private static final String CLAIM_TYPE = "claimType"; + private static final String FRIENDLY_NAME = "friendlyName"; + private static final String PERMISSIONS = "permissions"; + private static final String RESOURCE_SET = "resourceSet"; + private static final String PERMISSION_TICKETS = "permissionTickets"; + private static final String PERMISSION = "permission"; + private static final String TICKET = "ticket"; + private static final String CLAIMS_SUPPLIED = "claimsSupplied"; + private static final String SAVED_REGISTERED_CLIENTS = "savedRegisteredClients"; + private static final String RESOURCE_SETS = "resourceSets"; + private static final String TOKEN_PERMISSIONS = "tokenPermissions"; + private static final String TOKEN_ID = "tokenId"; + + private static final Logger logger = LoggerFactory.getLogger(UmaDataServiceExtension_1_3.class); + + + + @Autowired + private SavedRegisteredClientService registeredClientService; + @Autowired + private ResourceSetRepository resourceSetRepository; + @Autowired + private PermissionRepository permissionRepository; + @Autowired + private OAuth2TokenRepository tokenRepository; + + private Map> tokenToPermissionRefs = new HashMap<>(); + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataServiceExtension#supportsVersion(java.lang.String) + */ + @Override + public boolean supportsVersion(String version) { + return THIS_VERSION.equals(version); + + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataServiceExtension#exportExtensionData(com.google.gson.stream.JsonWriter) + */ + @Override + public void exportExtensionData(JsonWriter writer) throws IOException { + writer.name(SAVED_REGISTERED_CLIENTS); + writer.beginArray(); + writeSavedRegisteredClients(writer); + writer.endArray(); + + writer.name(RESOURCE_SETS); + writer.beginArray(); + writeResourceSets(writer); + writer.endArray(); + + writer.name(PERMISSION_TICKETS); + writer.beginArray(); + writePermissionTickets(writer); + writer.endArray(); + + writer.name(TOKEN_PERMISSIONS); + writer.beginArray(); + writeTokenPermissions(writer); + writer.endArray(); + } + + /** + * @param writer + * @throws IOException + */ + private void writeTokenPermissions(JsonWriter writer) throws IOException { + for (OAuth2AccessTokenEntity token : tokenRepository.getAllAccessTokens()) { + if (!token.getPermissions().isEmpty()) { // skip tokens that don't have the permissions structure attached + writer.beginObject(); + writer.name(TOKEN_ID).value(token.getId()); + writer.name(PERMISSIONS); + writer.beginArray(); + for (Permission p : token.getPermissions()) { + writer.beginObject(); + writer.name(RESOURCE_SET).value(p.getResourceSet().getId()); + writer.name(SCOPES); + writer.beginArray(); + for (String s : p.getScopes()) { + writer.value(s); + } + writer.endArray(); + writer.endObject(); + } + writer.endArray(); + + writer.endObject(); + } + } + } + + /** + * @param writer + * @throws IOException + */ + private void writePermissionTickets(JsonWriter writer) throws IOException { + for (PermissionTicket ticket : permissionRepository.getAll()) { + writer.beginObject(); + + writer.name(CLAIMS_SUPPLIED); + writer.beginArray(); + for (Claim claim : ticket.getClaimsSupplied()) { + writer.beginObject(); + + writer.name(ISSUER); + writer.beginArray(); + for (String issuer : claim.getIssuer()) { + writer.value(issuer); + } + writer.endArray(); + writer.name(CLAIM_TOKEN_FORMAT); + writer.beginArray(); + for (String format : claim.getClaimTokenFormat()) { + writer.value(format); + } + writer.endArray(); + writer.name(CLAIM_TYPE).value(claim.getClaimType()); + writer.name(FRIENDLY_NAME).value(claim.getFriendlyName()); + writer.name(NAME).value(claim.getName()); + writer.name(VALUE).value(claim.getValue().toString()); + writer.endObject(); + } + writer.endArray(); + + writer.name(EXPIRATION).value(toUTCString(ticket.getExpiration())); + + writer.name(PERMISSION); + writer.beginObject(); + Permission p = ticket.getPermission(); + writer.name(RESOURCE_SET).value(p.getResourceSet().getId()); + writer.name(SCOPES); + writer.beginArray(); + for (String s : p.getScopes()) { + writer.value(s); + } + writer.endArray(); + writer.endObject(); + + writer.name(TICKET).value(ticket.getTicket()); + + writer.endObject(); + } + + + } + + /** + * @param writer + * @throws IOException + */ + private void writeResourceSets(JsonWriter writer) throws IOException { + for (ResourceSet rs : resourceSetRepository.getAll()) { + writer.beginObject(); + writer.name(ID).value(rs.getId()); + writer.name(CLIENT_ID).value(rs.getClientId()); + writer.name(ICON_URI).value(rs.getIconUri()); + writer.name(NAME).value(rs.getName()); + writer.name(TYPE).value(rs.getType()); + writer.name(URI).value(rs.getUri()); + writer.name(OWNER).value(rs.getOwner()); + writer.name(POLICIES); + writer.beginArray(); + for (Policy policy : rs.getPolicies()) { + writer.beginObject(); + writer.name(NAME).value(policy.getName()); + writer.name(SCOPES); + writer.beginArray(); + for (String scope : policy.getScopes()) { + writer.value(scope); + } + writer.endArray(); + writer.name(CLAIMS_REQUIRED); + writer.beginArray(); + for (Claim claim : policy.getClaimsRequired()) { + writer.beginObject(); + + writer.name(ISSUER); + writer.beginArray(); + for (String issuer : claim.getIssuer()) { + writer.value(issuer); + } + writer.endArray(); + writer.name(CLAIM_TOKEN_FORMAT); + writer.beginArray(); + for (String format : claim.getClaimTokenFormat()) { + writer.value(format); + } + writer.endArray(); + writer.name(CLAIM_TYPE).value(claim.getClaimType()); + writer.name(FRIENDLY_NAME).value(claim.getFriendlyName()); + writer.name(NAME).value(claim.getName()); + writer.name(VALUE).value(claim.getValue().toString()); + writer.endObject(); + } + writer.endArray(); + writer.endObject(); + } + writer.endArray(); + writer.name(SCOPES); + writer.beginArray(); + for (String scope : rs.getScopes()) { + writer.value(scope); + } + writer.endArray(); + writer.endObject(); + logger.debug("Finished writing resource set {}", rs.getId()); + } + + } + + /** + * @param writer + */ + private void writeSavedRegisteredClients(JsonWriter writer) throws IOException { + for (SavedRegisteredClient src : registeredClientService.getAll()) { + writer.beginObject(); + writer.name(ISSUER).value(src.getIssuer()); + writer.name(REGISTERED_CLIENT).value(src.getRegisteredClient().getSource().toString()); + writer.endObject(); + logger.debug("Wrote saved registered client {}", src.getId()); + } + logger.info("Done writing saved registered clients"); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataServiceExtension#importExtensionData(com.google.gson.stream.JsonReader) + */ + @Override + public boolean importExtensionData(String name, JsonReader reader) throws IOException { + if (name.equals(SAVED_REGISTERED_CLIENTS)) { + readSavedRegisteredClients(reader); + return true; + } else if (name.equals(RESOURCE_SETS)) { + readResourceSets(reader); + return true; + } else if (name.equals(PERMISSION_TICKETS)) { + readPermissionTickets(reader); + return true; + } else if (name.equals(TOKEN_PERMISSIONS)) { + readTokenPermissions(reader); + return true; + } else { + return false; + } + } + + /** + * @param reader + */ + private void readTokenPermissions(JsonReader reader) throws IOException { + reader.beginArray(); + while(reader.hasNext()) { + reader.beginObject(); + Long tokenId = null; + Set permissions = new HashSet<>(); + while (reader.hasNext()) { + switch(reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (name.equals(TOKEN_ID)) { + tokenId = reader.nextLong(); + } else if (name.equals(PERMISSIONS)) { + reader.beginArray(); + while (reader.hasNext()) { + Permission p = new Permission(); + Long rsid = null; + Set scope = new HashSet<>(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String pname = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (pname.equals(RESOURCE_SET)) { + rsid = reader.nextLong(); + } else if (pname.equals(SCOPES)) { + scope = readSet(reader); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + p.setScopes(scope); + Permission saved = permissionRepository.saveRawPermission(p); + permissionToResourceRefs.put(saved.getId(), rsid); + permissions.add(saved.getId()); + } + reader.endArray(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + tokenToPermissionRefs.put(tokenId, permissions); + } + reader.endArray(); + + } + + private Map permissionToResourceRefs = new HashMap<>(); + + /** + * @param reader + */ + private void readPermissionTickets(JsonReader reader) throws IOException { + JsonParser parser = new JsonParser(); + reader.beginArray(); + while (reader.hasNext()) { + PermissionTicket ticket = new PermissionTicket(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(CLAIMS_SUPPLIED)) { + Set claimsSupplied = new HashSet<>(); + reader.beginArray(); + while (reader.hasNext()) { + Claim c = new Claim(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String cname = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (cname.equals(ISSUER)) { + c.setIssuer(readSet(reader)); + } else if (cname.equals(CLAIM_TOKEN_FORMAT)) { + c.setClaimTokenFormat(readSet(reader)); + } else if (cname.equals(CLAIM_TYPE)) { + c.setClaimType(reader.nextString()); + } else if (cname.equals(FRIENDLY_NAME)) { + c.setFriendlyName(reader.nextString()); + } else if (cname.equals(NAME)) { + c.setName(reader.nextString()); + } else if (cname.equals(VALUE)) { + JsonElement e = parser.parse(reader.nextString()); + c.setValue(e); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + claimsSupplied.add(c); + } + reader.endArray(); + ticket.setClaimsSupplied(claimsSupplied); + } else if (name.equals(EXPIRATION)) { + ticket.setExpiration(utcToDate(reader.nextString())); + } else if (name.equals(PERMISSION)) { + Permission p = new Permission(); + Long rsid = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String pname = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (pname.equals(RESOURCE_SET)) { + rsid = reader.nextLong(); + } else if (pname.equals(SCOPES)) { + p.setScopes(readSet(reader)); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Permission saved = permissionRepository.saveRawPermission(p); + permissionToResourceRefs.put(saved.getId(), rsid); + ticket.setPermission(saved); + } else if (name.equals(TICKET)) { + ticket.setTicket(reader.nextString()); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + permissionRepository.save(ticket); + } + reader.endArray(); + } + + + private Map resourceSetOldToNewIdMap = new HashMap<>(); + + /** + * @param reader + */ + private void readResourceSets(JsonReader reader) throws IOException { + JsonParser parser = new JsonParser(); + reader.beginArray(); + while (reader.hasNext()) { + Long oldId = null; + ResourceSet rs = new ResourceSet(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ID)) { + oldId = reader.nextLong(); + } else if (name.equals(CLIENT_ID)) { + rs.setClientId(reader.nextString()); + } else if (name.equals(ICON_URI)) { + rs.setIconUri(reader.nextString()); + } else if (name.equals(NAME)) { + rs.setName(reader.nextString()); + } else if (name.equals(TYPE)) { + rs.setType(reader.nextString()); + } else if (name.equals(URI)) { + rs.setUri(reader.nextString()); + } else if (name.equals(OWNER)) { + rs.setOwner(reader.nextString()); + } else if (name.equals(POLICIES)) { + Set policies = new HashSet<>(); + reader.beginArray(); + while (reader.hasNext()) { + Policy p = new Policy(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String pname = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (pname.equals(NAME)) { + p.setName(reader.nextString()); + } else if (pname.equals(SCOPES)) { + p.setScopes(readSet(reader)); + } else if (pname.equals(CLAIMS_REQUIRED)) { + Set claimsRequired = new HashSet<>(); + reader.beginArray(); + while (reader.hasNext()) { + Claim c = new Claim(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String cname = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (cname.equals(ISSUER)) { + c.setIssuer(readSet(reader)); + } else if (cname.equals(CLAIM_TOKEN_FORMAT)) { + c.setClaimTokenFormat(readSet(reader)); + } else if (cname.equals(CLAIM_TYPE)) { + c.setClaimType(reader.nextString()); + } else if (cname.equals(FRIENDLY_NAME)) { + c.setFriendlyName(reader.nextString()); + } else if (cname.equals(NAME)) { + c.setName(reader.nextString()); + } else if (cname.equals(VALUE)) { + JsonElement e = parser.parse(reader.nextString()); + c.setValue(e); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + claimsRequired.add(c); + } + reader.endArray(); + p.setClaimsRequired(claimsRequired); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + policies.add(p); + } + reader.endArray(); + rs.setPolicies(policies); + } else if (name.equals(SCOPES)) { + rs.setScopes(readSet(reader)); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + Long newId = resourceSetRepository.save(rs).getId(); + resourceSetOldToNewIdMap.put(oldId, newId); + } + reader.endArray(); + logger.info("Done reading resource sets"); + } + + /** + * @param reader + */ + private void readSavedRegisteredClients(JsonReader reader) throws IOException{ + reader.beginArray(); + while (reader.hasNext()) { + String issuer = null; + String clientString = null; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.peek()) { + case END_OBJECT: + continue; + case NAME: + String name = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + } else if (name.equals(ISSUER)) { + issuer = reader.nextString(); + } else if (name.equals(REGISTERED_CLIENT)) { + clientString = reader.nextString(); + } else { + logger.debug("Found unexpected entry"); + reader.skipValue(); + } + break; + default: + logger.debug("Found unexpected entry"); + reader.skipValue(); + continue; + } + } + reader.endObject(); + RegisteredClient client = ClientDetailsEntityJsonProcessor.parseRegistered(clientString); + registeredClientService.save(issuer, client); + logger.debug("Saved registered client"); + } + reader.endArray(); + logger.info("Done reading saved registered clients"); + } + + /* (non-Javadoc) + * @see org.mitre.openid.connect.service.MITREidDataServiceExtension#fixExtensionObjectReferences() + */ + @Override + public void fixExtensionObjectReferences(MITREidDataServiceMaps maps) { + for (Long permissionId : permissionToResourceRefs.keySet()) { + Long oldResourceId = permissionToResourceRefs.get(permissionId); + Long newResourceId = resourceSetOldToNewIdMap.get(oldResourceId); + Permission p = permissionRepository.getById(permissionId); + ResourceSet rs = resourceSetRepository.getById(newResourceId); + p.setResourceSet(rs); + permissionRepository.saveRawPermission(p); + logger.debug("Mapping rsid " + oldResourceId + " to " + newResourceId + " for permission " + permissionId); + } + for (Long tokenId : tokenToPermissionRefs.keySet()) { + Long newTokenId = maps.getAccessTokenOldToNewIdMap().get(tokenId); + OAuth2AccessTokenEntity token = tokenRepository.getAccessTokenById(newTokenId); + + Set permissions = new HashSet<>(); + for (Long permissionId : tokenToPermissionRefs.get(tokenId)) { + Permission p = permissionRepository.getById(permissionId); + permissions.add(p); + } + + token.setPermissions(permissions); + tokenRepository.saveAccessToken(token); + } + permissionToResourceRefs.clear(); + resourceSetOldToNewIdMap.clear(); + tokenToPermissionRefs.clear(); + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/util/ExternalLoginAuthoritiesMapper.java b/uma-server/src/main/java/org/mitre/uma/util/ExternalLoginAuthoritiesMapper.java new file mode 100644 index 0000000000..9626eba04a --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/util/ExternalLoginAuthoritiesMapper.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.util; + +import java.util.Collection; + +import org.mitre.openid.connect.client.OIDCAuthoritiesMapper; +import org.mitre.openid.connect.model.UserInfo; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import com.google.common.collect.Sets; +import com.nimbusds.jwt.JWT; + +/** + * Utility class to map all external logins to the ROLE_EXTERNAL_USER authority + * to prevent them from accessing other parts of the server. + * + * @author jricher + * + */ +public class ExternalLoginAuthoritiesMapper implements OIDCAuthoritiesMapper { + + private static final GrantedAuthority ROLE_EXTERNAL_USER = new SimpleGrantedAuthority("ROLE_EXTERNAL_USER"); + + @Override + public Collection mapAuthorities(JWT idToken, UserInfo userInfo) { + return Sets.newHashSet(ROLE_EXTERNAL_USER); + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/view/ResourceSetEntityAbbreviatedView.java b/uma-server/src/main/java/org/mitre/uma/view/ResourceSetEntityAbbreviatedView.java new file mode 100644 index 0000000000..9f581fb67c --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/view/ResourceSetEntityAbbreviatedView.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.view; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.uma.model.ResourceSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.web.servlet.view.AbstractView; + +import com.google.common.base.Strings; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.LongSerializationPolicy; + +@Component(ResourceSetEntityAbbreviatedView.VIEWNAME) +public class ResourceSetEntityAbbreviatedView extends AbstractView { + private static Logger logger = LoggerFactory.getLogger(JsonEntityView.class); + + public static final String VIEWNAME = "resourceSetEntityAbbreviatedView"; + + public static final String LOCATION = "location"; + + @Autowired + private ConfigurationPropertiesBean config; + + private Gson gson = new GsonBuilder() + .setExclusionStrategies(new ExclusionStrategy() { + + @Override + public boolean shouldSkipField(FieldAttributes f) { + + return false; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } + return false; + } + + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .setLongSerializationPolicy(LongSerializationPolicy.STRING) + .create(); + + @Override + protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { + + response.setContentType("application/json"); + + + HttpStatus code = (HttpStatus) model.get(HttpCodeView.CODE); + if (code == null) { + code = HttpStatus.OK; // default to 200 + } + + response.setStatus(code.value()); + + String location = (String) model.get(LOCATION); + if (!Strings.isNullOrEmpty(location)) { + response.setHeader(HttpHeaders.LOCATION, location); + } + + try { + + Writer out = response.getWriter(); + ResourceSet rs = (ResourceSet) model.get(JsonEntityView.ENTITY); + + JsonObject o = new JsonObject(); + + o.addProperty("_id", rs.getId().toString()); // set the ID to a string + o.addProperty("user_access_policy_uri", config.getIssuer() + "manage/user/policy/" + rs.getId()); + + + gson.toJson(o, out); + + } catch (IOException e) { + + logger.error("IOException in ResourceSetEntityView.java: ", e); + + } + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/view/ResourceSetEntityView.java b/uma-server/src/main/java/org/mitre/uma/view/ResourceSetEntityView.java new file mode 100644 index 0000000000..e34e474313 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/view/ResourceSetEntityView.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.view; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.uma.model.ResourceSet; +import org.mitre.util.JsonUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.web.servlet.view.AbstractView; + +import com.google.common.base.Strings; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.LongSerializationPolicy; + +@Component(ResourceSetEntityView.VIEWNAME) +public class ResourceSetEntityView extends AbstractView { + private static Logger logger = LoggerFactory.getLogger(JsonEntityView.class); + + public static final String VIEWNAME = "resourceSetEntityView"; + + @Autowired + private ConfigurationPropertiesBean config; + + private Gson gson = new GsonBuilder() + .setExclusionStrategies(new ExclusionStrategy() { + + @Override + public boolean shouldSkipField(FieldAttributes f) { + + return false; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } + return false; + } + + }) + .serializeNulls() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .setLongSerializationPolicy(LongSerializationPolicy.STRING) + .create(); + + @Override + protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) { + + response.setContentType("application/json"); + + + HttpStatus code = (HttpStatus) model.get("code"); + if (code == null) { + code = HttpStatus.OK; // default to 200 + } + + response.setStatus(code.value()); + + String location = (String) model.get("location"); + if (!Strings.isNullOrEmpty(location)) { + response.setHeader(HttpHeaders.LOCATION, location); + } + + try { + + Writer out = response.getWriter(); + ResourceSet rs = (ResourceSet) model.get("entity"); + + JsonObject o = new JsonObject(); + + o.addProperty("_id", rs.getId().toString()); // send the id as a string + o.addProperty("user_access_policy_uri", config.getIssuer() + "manage/resource/" + rs.getId()); + o.addProperty("name", rs.getName()); + o.addProperty("uri", rs.getUri()); + o.addProperty("type", rs.getType()); + o.add("scopes", JsonUtils.getAsArray(rs.getScopes())); + o.addProperty("icon_uri", rs.getIconUri()); + + gson.toJson(o, out); + + } catch (IOException e) { + + logger.error("IOException in ResourceSetEntityView.java: ", e); + + } + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/web/AuthorizationRequestEndpoint.java b/uma-server/src/main/java/org/mitre/uma/web/AuthorizationRequestEndpoint.java new file mode 100644 index 0000000000..04f8378445 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/web/AuthorizationRequestEndpoint.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.web; + +import java.util.Map; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.oauth2.web.AuthenticationUtilities; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.uma.model.Claim; +import org.mitre.uma.model.ClaimProcessingResult; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ClaimsProcessingService; +import org.mitre.uma.service.PermissionService; +import org.mitre.uma.service.UmaTokenService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + +/** + * @author jricher + * + */ +@Controller +@RequestMapping("/" + AuthorizationRequestEndpoint.URL) +public class AuthorizationRequestEndpoint { + // Logger for this class + private static final Logger logger = LoggerFactory.getLogger(AuthorizationRequestEndpoint.class); + + public static final String RPT = "rpt"; + public static final String TICKET = "ticket"; + public static final String URL = "authz_request"; + + @Autowired + private PermissionService permissionService; + + @Autowired + private OAuth2TokenEntityService tokenService; + + @Autowired + private ClaimsProcessingService claimsProcessingService; + + @Autowired + private UmaTokenService umaTokenService; + + @RequestMapping(method = RequestMethod.POST, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String authorizationRequest(@RequestBody String jsonString, Model m, Authentication auth) { + + AuthenticationUtilities.ensureOAuthScope(auth, SystemScopeService.UMA_AUTHORIZATION_SCOPE); + + JsonParser parser = new JsonParser(); + JsonElement e = parser.parse(jsonString); + + if (e.isJsonObject()) { + JsonObject o = e.getAsJsonObject(); + + if (o.has(TICKET)) { + + OAuth2AccessTokenEntity incomingRpt = null; + if (o.has(RPT)) { + String rptValue = o.get(RPT).getAsString(); + incomingRpt = tokenService.readAccessToken(rptValue); + } + + String ticketValue = o.get(TICKET).getAsString(); + + PermissionTicket ticket = permissionService.getByTicket(ticketValue); + + if (ticket != null) { + // found the ticket, see if it's any good + + ResourceSet rs = ticket.getPermission().getResourceSet(); + + if (rs.getPolicies() == null || rs.getPolicies().isEmpty()) { + // the required claims are empty, this resource has no way to be authorized + + m.addAttribute(JsonErrorView.ERROR, "not_authorized"); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "This resource set can not be accessed."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return JsonErrorView.VIEWNAME; + } else { + // claims weren't empty or missing, we need to check against what we have + + ClaimProcessingResult result = claimsProcessingService.claimsAreSatisfied(rs, ticket); + + + if (result.isSatisfied()) { + // the service found what it was looking for, issue a token + + // we need to downscope this based on the required set that was matched if it was matched + OAuth2Authentication o2auth = (OAuth2Authentication) auth; + + OAuth2AccessTokenEntity token = umaTokenService.createRequestingPartyToken(o2auth, ticket, result.getMatched()); + + // if we have an inbound RPT, throw it out because we're replacing it + if (incomingRpt != null) { + tokenService.revokeAccessToken(incomingRpt); + } + + Map entity = ImmutableMap.of("rpt", token.getValue()); + + m.addAttribute(JsonEntityView.ENTITY, entity); + + return JsonEntityView.VIEWNAME; + + } else { + + // if we got here, the claim didn't match, forward the user to the claim gathering endpoint + JsonObject entity = new JsonObject(); + + entity.addProperty(JsonErrorView.ERROR, "need_info"); + JsonObject details = new JsonObject(); + + JsonObject rpClaims = new JsonObject(); + rpClaims.addProperty("redirect_user", true); + rpClaims.addProperty("ticket", ticketValue); + JsonArray req = new JsonArray(); + for (Claim claim : result.getUnmatched()) { + JsonObject c = new JsonObject(); + c.addProperty("name", claim.getName()); + c.addProperty("friendly_name", claim.getFriendlyName()); + c.addProperty("claim_type", claim.getClaimType()); + JsonArray f = new JsonArray(); + for (String format : claim.getClaimTokenFormat()) { + f.add(new JsonPrimitive(format)); + } + c.add("claim_token_format", f); + JsonArray i = new JsonArray(); + for (String issuer : claim.getIssuer()) { + i.add(new JsonPrimitive(issuer)); + } + c.add("issuer", i); + req.add(c); + } + rpClaims.add("required_claims", req); + details.add("requesting_party_claims", rpClaims); + entity.add("error_details", details); + + m.addAttribute(JsonEntityView.ENTITY, entity); + return JsonEntityView.VIEWNAME; + } + + + } + } else { + // ticket wasn't found, return an error + m.addAttribute(HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR, "invalid_ticket"); + return JsonErrorView.VIEWNAME; + } + + } else { + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Missing JSON elements."); + return JsonErrorView.VIEWNAME; + } + + + } else { + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Malformed JSON request."); + return JsonErrorView.VIEWNAME; + } + + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/web/ClaimsCollectionEndpoint.java b/uma-server/src/main/java/org/mitre/uma/web/ClaimsCollectionEndpoint.java new file mode 100644 index 0000000000..52061e4ebf --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/web/ClaimsCollectionEndpoint.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.web; + +import java.util.Set; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.model.OIDCAuthenticationToken; +import org.mitre.openid.connect.model.UserInfo; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.uma.model.Claim; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.service.PermissionService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponentsBuilder; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +/** + * + * Collect claims interactively from the end user. + * + * @author jricher + * + */ +@Controller +@PreAuthorize("hasRole('ROLE_EXTERNAL_USER')") +@RequestMapping("/" + ClaimsCollectionEndpoint.URL) +public class ClaimsCollectionEndpoint { + // Logger for this class + private static final Logger logger = LoggerFactory.getLogger(ClaimsCollectionEndpoint.class); + + public static final String URL = "rqp_claims"; + + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private PermissionService permissionService; + + + @RequestMapping(method = RequestMethod.GET) + public String collectClaims(@RequestParam("client_id") String clientId, @RequestParam(value = "redirect_uri", required = false) String redirectUri, + @RequestParam("ticket") String ticketValue, @RequestParam(value = "state", required = false) String state, + Model m, OIDCAuthenticationToken auth) { + + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + PermissionTicket ticket = permissionService.getByTicket(ticketValue); + + if (client == null || ticket == null) { + logger.info("Client or ticket not found: " + clientId + " :: " + ticketValue); + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + // we've got a client and ticket, let's attach the claims that we have from the token and userinfo + + // subject + Set claimsSupplied = Sets.newHashSet(ticket.getClaimsSupplied()); + + String issuer = auth.getIssuer(); + UserInfo userInfo = auth.getUserInfo(); + + claimsSupplied.add(mkClaim(issuer, "sub", new JsonPrimitive(auth.getSub()))); + if (userInfo.getEmail() != null) { + claimsSupplied.add(mkClaim(issuer, "email", new JsonPrimitive(userInfo.getEmail()))); + } + if (userInfo.getEmailVerified() != null) { + claimsSupplied.add(mkClaim(issuer, "email_verified", new JsonPrimitive(userInfo.getEmailVerified()))); + } + if (userInfo.getPhoneNumber() != null) { + claimsSupplied.add(mkClaim(issuer, "phone_number", new JsonPrimitive(auth.getUserInfo().getPhoneNumber()))); + } + if (userInfo.getPhoneNumberVerified() != null) { + claimsSupplied.add(mkClaim(issuer, "phone_number_verified", new JsonPrimitive(auth.getUserInfo().getPhoneNumberVerified()))); + } + if (userInfo.getPreferredUsername() != null) { + claimsSupplied.add(mkClaim(issuer, "preferred_username", new JsonPrimitive(auth.getUserInfo().getPreferredUsername()))); + } + if (userInfo.getProfile() != null) { + claimsSupplied.add(mkClaim(issuer, "profile", new JsonPrimitive(auth.getUserInfo().getProfile()))); + } + + ticket.setClaimsSupplied(claimsSupplied); + + PermissionTicket updatedTicket = permissionService.updateTicket(ticket); + + if (Strings.isNullOrEmpty(redirectUri)) { + if (client.getClaimsRedirectUris().size() == 1) { + redirectUri = client.getClaimsRedirectUris().iterator().next(); // get the first (and only) redirect URI to use here + logger.info("No redirect URI passed in, using registered value: " + redirectUri); + } else { + throw new RedirectMismatchException("Unable to find redirect URI and none passed in."); + } + } else { + if (!client.getClaimsRedirectUris().contains(redirectUri)) { + throw new RedirectMismatchException("Claims redirect did not match the registered values."); + } + } + + UriComponentsBuilder template = UriComponentsBuilder.fromUriString(redirectUri); + template.queryParam("authorization_state", "claims_submitted"); + if (!Strings.isNullOrEmpty(state)) { + template.queryParam("state", state); + } + + String uriString = template.toUriString(); + logger.info("Redirecting to " + uriString); + + return "redirect:" + uriString; + } + + + private Claim mkClaim(String issuer, String name, JsonElement value) { + Claim c = new Claim(); + c.setIssuer(Sets.newHashSet(issuer)); + c.setName(name); + c.setValue(value); + return c; + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/web/PermissionRegistrationEndpoint.java b/uma-server/src/main/java/org/mitre/uma/web/PermissionRegistrationEndpoint.java new file mode 100644 index 0000000000..a3b6601294 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/web/PermissionRegistrationEndpoint.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.web; + +import static org.mitre.oauth2.web.AuthenticationUtilities.ensureOAuthScope; +import static org.mitre.util.JsonUtils.getAsLong; +import static org.mitre.util.JsonUtils.getAsStringSet; + +import java.util.Set; + +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.PermissionService; +import org.mitre.uma.service.ResourceSetService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +/** + * @author jricher + * + */ +@Controller +@RequestMapping("/" + PermissionRegistrationEndpoint.URL) +@PreAuthorize("hasRole('ROLE_USER')") +public class PermissionRegistrationEndpoint { + // Logger for this class + private static final Logger logger = LoggerFactory.getLogger(PermissionRegistrationEndpoint.class); + + public static final String URL = "permission"; + + @Autowired + private PermissionService permissionService; + + @Autowired + private ResourceSetService resourceSetService; + + @Autowired + private SystemScopeService scopeService; + + private JsonParser parser = new JsonParser(); + + @RequestMapping(method = RequestMethod.POST, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String getPermissionTicket(@RequestBody String jsonString, Model m, Authentication auth) { + + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + + try { + + // parse the permission request + + JsonElement el = parser.parse(jsonString); + if (el.isJsonObject()) { + JsonObject o = el.getAsJsonObject(); + + Long rsid = getAsLong(o, "resource_set_id"); + Set scopes = getAsStringSet(o, "scopes"); + + if (rsid == null || scopes == null || scopes.isEmpty()){ + // missing information + m.addAttribute("code", HttpStatus.BAD_REQUEST); + m.addAttribute("errorMessage", "Missing required component of permission registration request."); + return JsonErrorView.VIEWNAME; + } + + // trim any restricted scopes + Set scopesRequested = scopeService.fromStrings(scopes); + scopesRequested = scopeService.removeRestrictedAndReservedScopes(scopesRequested); + scopes = scopeService.toStrings(scopesRequested); + + ResourceSet resourceSet = resourceSetService.getById(rsid); + + // requested resource set doesn't exist + if (resourceSet == null) { + m.addAttribute("code", HttpStatus.NOT_FOUND); + m.addAttribute("errorMessage", "Requested resource set not found: " + rsid); + return JsonErrorView.VIEWNAME; + } + + // authorized user of the token doesn't match owner of the resource set + if (!resourceSet.getOwner().equals(auth.getName())) { + m.addAttribute("code", HttpStatus.FORBIDDEN); + m.addAttribute("errorMessage", "Party requesting permission is not owner of resource set, expected " + resourceSet.getOwner() + " got " + auth.getName()); + return JsonErrorView.VIEWNAME; + } + + // create the permission + PermissionTicket permission = permissionService.createTicket(resourceSet, scopes); + + if (permission != null) { + // we've created the permission, return the ticket + JsonObject out = new JsonObject(); + out.addProperty("ticket", permission.getTicket()); + m.addAttribute("entity", out); + + m.addAttribute("code", HttpStatus.CREATED); + + return JsonEntityView.VIEWNAME; + } else { + // there was a failure creating the permission object + + m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR); + m.addAttribute("errorMessage", "Unable to save permission and generate ticket."); + + return JsonErrorView.VIEWNAME; + } + + } else { + // malformed request + m.addAttribute("code", HttpStatus.BAD_REQUEST); + m.addAttribute("errorMessage", "Malformed JSON request."); + return JsonErrorView.VIEWNAME; + } + } catch (JsonParseException e) { + // malformed request + m.addAttribute("code", HttpStatus.BAD_REQUEST); + m.addAttribute("errorMessage", "Malformed JSON request."); + return JsonErrorView.VIEWNAME; + } + + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/web/PolicyAPI.java b/uma-server/src/main/java/org/mitre/uma/web/PolicyAPI.java new file mode 100644 index 0000000000..2b1feda583 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/web/PolicyAPI.java @@ -0,0 +1,391 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.web; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.openid.connect.web.RootController; +import org.mitre.uma.model.Claim; +import org.mitre.uma.model.Policy; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ResourceSetService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.google.common.collect.Sets; +import com.google.gson.Gson; + +/** + * API for managing policies on resource sets. + * + * @author jricher + * + */ +@Controller +@RequestMapping("/" + PolicyAPI.URL) +@PreAuthorize("hasRole('ROLE_USER')") +public class PolicyAPI { + + // Logger for this class + private static final Logger logger = LoggerFactory.getLogger(PolicyAPI.class); + + public static final String URL = RootController.API_URL + "/resourceset"; + public static final String POLICYURL = "/policy"; + + private Gson gson = new Gson(); + + @Autowired + private ResourceSetService resourceSetService; + + /** + * List all resource sets for the current user + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "", method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String getResourceSetsForCurrentUser(Model m, Authentication auth) { + + Collection resourceSets = resourceSetService.getAllForOwner(auth.getName()); + + m.addAttribute(JsonEntityView.ENTITY, resourceSets); + + return JsonEntityView.VIEWNAME; + } + + /** + * Get the indicated resource set + * @param rsid + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "/{rsid}", method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String getResourceSet(@PathVariable (value = "rsid") Long rsid, Model m, Authentication auth) { + + ResourceSet rs = resourceSetService.getById(rsid); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + if (!rs.getOwner().equals(auth.getName())) { + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // authenticated user didn't match the owner of the resource set + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + } + + m.addAttribute(JsonEntityView.ENTITY, rs); + + return JsonEntityView.VIEWNAME; + } + + /** + * Delete the indicated resource set + * @param rsid + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "/{rsid}", method = RequestMethod.DELETE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String deleteResourceSet(@PathVariable (value = "rsid") Long rsid, Model m, Authentication auth) { + + ResourceSet rs = resourceSetService.getById(rsid); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + if (!rs.getOwner().equals(auth.getName())) { + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // authenticated user didn't match the owner of the resource set + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + } + + resourceSetService.remove(rs); + m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); + return HttpCodeView.VIEWNAME; + + } + + /** + * List all the policies for the given resource set + * @param rsid + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "/{rsid}" + POLICYURL, method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String getPoliciesForResourceSet(@PathVariable (value = "rsid") Long rsid, Model m, Authentication auth) { + + ResourceSet rs = resourceSetService.getById(rsid); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + if (!rs.getOwner().equals(auth.getName())) { + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // authenticated user didn't match the owner of the resource set + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + } + + m.addAttribute(JsonEntityView.ENTITY, rs.getPolicies()); + + return JsonEntityView.VIEWNAME; + } + + /** + * Create a new policy on the given resource set + * @param rsid + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "/{rsid}" + POLICYURL, method = RequestMethod.POST, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String createNewPolicyForResourceSet(@PathVariable (value = "rsid") Long rsid, @RequestBody String jsonString, Model m, Authentication auth) { + ResourceSet rs = resourceSetService.getById(rsid); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + if (!rs.getOwner().equals(auth.getName())) { + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // authenticated user didn't match the owner of the resource set + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + } + + Policy p = gson.fromJson(jsonString, Policy.class); + + if (p.getId() != null) { + logger.warn("Tried to add a policy with a non-null ID: " + p.getId()); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + return HttpCodeView.VIEWNAME; + } + + for (Claim claim : p.getClaimsRequired()) { + if (claim.getId() != null) { + logger.warn("Tried to add a policy with a non-null claim ID: " + claim.getId()); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + return HttpCodeView.VIEWNAME; + } + } + + rs.getPolicies().add(p); + ResourceSet saved = resourceSetService.update(rs, rs); + + // find the new policy object + Collection newPolicies = Sets.difference(new HashSet<>(saved.getPolicies()), new HashSet<>(rs.getPolicies())); + + if (newPolicies.size() == 1) { + Policy newPolicy = newPolicies.iterator().next(); + m.addAttribute(JsonEntityView.ENTITY, newPolicy); + return JsonEntityView.VIEWNAME; + } else { + logger.warn("Unexpected result trying to add a new policy object: " + newPolicies); + m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); + return HttpCodeView.VIEWNAME; + } + + } + + /** + * Get a specific policy + * @param rsid + * @param pid + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "/{rsid}" + POLICYURL + "/{pid}", method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String getPolicy(@PathVariable (value = "rsid") Long rsid, @PathVariable (value = "pid") Long pid, Model m, Authentication auth) { + + ResourceSet rs = resourceSetService.getById(rsid); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + if (!rs.getOwner().equals(auth.getName())) { + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // authenticated user didn't match the owner of the resource set + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + } + + for (Policy policy : rs.getPolicies()) { + if (policy.getId().equals(pid)) { + // found it! + m.addAttribute(JsonEntityView.ENTITY, policy); + return JsonEntityView.VIEWNAME; + } + } + + // if we made it this far, we haven't found it + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + /** + * Update a specific policy + * @param rsid + * @param pid + * @param jsonString + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "/{rsid}" + POLICYURL + "/{pid}", method = RequestMethod.PUT, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String setClaimsForResourceSet(@PathVariable (value = "rsid") Long rsid, @PathVariable (value = "pid") Long pid, @RequestBody String jsonString, Model m, Authentication auth) { + + ResourceSet rs = resourceSetService.getById(rsid); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + if (!rs.getOwner().equals(auth.getName())) { + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // authenticated user didn't match the owner of the resource set + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return HttpCodeView.VIEWNAME; + } + + Policy p = gson.fromJson(jsonString, Policy.class); + + if (!pid.equals(p.getId())) { + logger.warn("Policy ID mismatch, expected " + pid + " got " + p.getId()); + + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + return HttpCodeView.VIEWNAME; + } + + for (Policy policy : rs.getPolicies()) { + if (policy.getId().equals(pid)) { + // found it! + + // find the existing claim IDs, make sure we're not overwriting anything from another policy + Set claimIds = new HashSet<>(); + for (Claim claim : policy.getClaimsRequired()) { + claimIds.add(claim.getId()); + } + + for (Claim claim : p.getClaimsRequired()) { + if (claim.getId() != null && !claimIds.contains(claim.getId())) { + logger.warn("Tried to add a policy with a an unmatched claim ID: got " + claim.getId() + " expected " + claimIds); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + return HttpCodeView.VIEWNAME; + } + } + + // update the existing object with the new values + policy.setClaimsRequired(p.getClaimsRequired()); + policy.setName(p.getName()); + policy.setScopes(p.getScopes()); + + resourceSetService.update(rs, rs); + + m.addAttribute(JsonEntityView.ENTITY, policy); + return JsonEntityView.VIEWNAME; + } + } + + // if we made it this far, we haven't found it + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + } + + /** + * Delete a specific policy + * @param rsid + * @param pid + * @param m + * @param auth + * @return + */ + @RequestMapping(value = "/{rsid}" + POLICYURL + "/{pid}", method = RequestMethod.DELETE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String deleteResourceSet(@PathVariable ("rsid") Long rsid, @PathVariable (value = "pid") Long pid, Model m, Authentication auth) { + + ResourceSet rs = resourceSetService.getById(rsid); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.addAttribute(JsonErrorView.ERROR, "not_found"); + return JsonErrorView.VIEWNAME; + } + + if (!auth.getName().equals(rs.getOwner())) { + + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // it wasn't issued to this user + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return JsonErrorView.VIEWNAME; + } + + + for (Policy policy : rs.getPolicies()) { + if (policy.getId().equals(pid)) { + // found it! + rs.getPolicies().remove(policy); + resourceSetService.update(rs, rs); + + m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); + return HttpCodeView.VIEWNAME; + } + } + + // if we made it this far, we haven't found it + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return HttpCodeView.VIEWNAME; + + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/web/ResourceSetRegistrationEndpoint.java b/uma-server/src/main/java/org/mitre/uma/web/ResourceSetRegistrationEndpoint.java new file mode 100644 index 0000000000..ce10568fba --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/web/ResourceSetRegistrationEndpoint.java @@ -0,0 +1,317 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.web; + + +import static org.mitre.oauth2.web.AuthenticationUtilities.ensureOAuthScope; +import static org.mitre.util.JsonUtils.getAsLong; +import static org.mitre.util.JsonUtils.getAsString; +import static org.mitre.util.JsonUtils.getAsStringSet; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.mitre.oauth2.model.SystemScope; +import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ResourceSetService; +import org.mitre.uma.view.ResourceSetEntityAbbreviatedView; +import org.mitre.uma.view.ResourceSetEntityView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.google.common.base.Strings; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +@Controller +@RequestMapping("/" + ResourceSetRegistrationEndpoint.URL) +@PreAuthorize("hasRole('ROLE_USER')") +public class ResourceSetRegistrationEndpoint { + + private static final Logger logger = LoggerFactory.getLogger(ResourceSetRegistrationEndpoint.class); + + public static final String DISCOVERY_URL = "resource_set"; + public static final String URL = DISCOVERY_URL + "/resource_set"; + + @Autowired + private ResourceSetService resourceSetService; + + @Autowired + private ConfigurationPropertiesBean config; + + @Autowired + private SystemScopeService scopeService; + + private JsonParser parser = new JsonParser(); + + @RequestMapping(method = RequestMethod.POST, produces = MimeTypeUtils.APPLICATION_JSON_VALUE, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String createResourceSet(@RequestBody String jsonString, Model m, Authentication auth) { + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + + ResourceSet rs = parseResourceSet(jsonString); + + if (rs == null) { // there was no resource set in the body + logger.warn("Resource set registration missing body."); + + m.addAttribute("code", HttpStatus.BAD_REQUEST); + m.addAttribute("error_description", "Resource request was missing body."); + return JsonErrorView.VIEWNAME; + } + + if (auth instanceof OAuth2Authentication) { + // if it's an OAuth mediated call, it's on behalf of a client, so store that + OAuth2Authentication o2a = (OAuth2Authentication) auth; + rs.setClientId(o2a.getOAuth2Request().getClientId()); + rs.setOwner(auth.getName()); // the username is going to be in the auth object + } else { + // this one shouldn't be called if it's not OAuth + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "This call must be made with an OAuth token"); + return JsonErrorView.VIEWNAME; + } + + rs = validateScopes(rs); + + if (Strings.isNullOrEmpty(rs.getName()) // there was no name (required) + || rs.getScopes() == null // there were no scopes (required) + ) { + + logger.warn("Resource set registration missing one or more required fields."); + + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Resource request was missing one or more required fields."); + return JsonErrorView.VIEWNAME; + } + + ResourceSet saved = resourceSetService.saveNew(rs); + + m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); + m.addAttribute(JsonEntityView.ENTITY, saved); + m.addAttribute(ResourceSetEntityAbbreviatedView.LOCATION, config.getIssuer() + URL + "/" + saved.getId()); + + return ResourceSetEntityAbbreviatedView.VIEWNAME; + + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String readResourceSet(@PathVariable ("id") Long id, Model m, Authentication auth) { + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + + ResourceSet rs = resourceSetService.getById(id); + + if (rs == null) { + m.addAttribute("code", HttpStatus.NOT_FOUND); + m.addAttribute("error", "not_found"); + return JsonErrorView.VIEWNAME; + } else { + + rs = validateScopes(rs); + + if (!auth.getName().equals(rs.getOwner())) { + + logger.warn("Unauthorized resource set request from wrong user; expected " + rs.getOwner() + " got " + auth.getName()); + + // it wasn't issued to this user + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return JsonErrorView.VIEWNAME; + } else { + m.addAttribute(JsonEntityView.ENTITY, rs); + return ResourceSetEntityView.VIEWNAME; + } + + } + + } + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String updateResourceSet(@PathVariable ("id") Long id, @RequestBody String jsonString, Model m, Authentication auth) { + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + + ResourceSet newRs = parseResourceSet(jsonString); + + if (newRs == null // there was no resource set in the body + || Strings.isNullOrEmpty(newRs.getName()) // there was no name (required) + || newRs.getScopes() == null // there were no scopes (required) + || newRs.getId() == null || !newRs.getId().equals(id) // the IDs didn't match + ) { + + logger.warn("Resource set registration missing one or more required fields."); + + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Resource request was missing one or more required fields."); + return JsonErrorView.VIEWNAME; + } + + ResourceSet rs = resourceSetService.getById(id); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.addAttribute(JsonErrorView.ERROR, "not_found"); + return JsonErrorView.VIEWNAME; + } else { + if (!auth.getName().equals(rs.getOwner())) { + + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // it wasn't issued to this user + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return JsonErrorView.VIEWNAME; + } else { + + ResourceSet saved = resourceSetService.update(rs, newRs); + + m.addAttribute(JsonEntityView.ENTITY, saved); + m.addAttribute(ResourceSetEntityAbbreviatedView.LOCATION, config.getIssuer() + URL + "/" + rs.getId()); + return ResourceSetEntityAbbreviatedView.VIEWNAME; + } + + } + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String deleteResourceSet(@PathVariable ("id") Long id, Model m, Authentication auth) { + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + + ResourceSet rs = resourceSetService.getById(id); + + if (rs == null) { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + m.addAttribute(JsonErrorView.ERROR, "not_found"); + return JsonErrorView.VIEWNAME; + } else { + if (!auth.getName().equals(rs.getOwner())) { + + logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); + + // it wasn't issued to this user + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return JsonErrorView.VIEWNAME; + } else if (auth instanceof OAuth2Authentication && + !((OAuth2Authentication)auth).getOAuth2Request().getClientId().equals(rs.getClientId())){ + + logger.warn("Unauthorized resource set request from bad client; expected " + rs.getClientId() + " got " + ((OAuth2Authentication)auth).getOAuth2Request().getClientId()); + + // it wasn't issued to this client + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return JsonErrorView.VIEWNAME; + } else { + + // user and client matched + resourceSetService.remove(rs); + + m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); + return HttpCodeView.VIEWNAME; + } + + } + } + + @RequestMapping(method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String listResourceSets(Model m, Authentication auth) { + ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); + + String owner = auth.getName(); + + Collection resourceSets = Collections.emptySet(); + if (auth instanceof OAuth2Authentication) { + // if it's an OAuth mediated call, it's on behalf of a client, so look that up too + OAuth2Authentication o2a = (OAuth2Authentication) auth; + resourceSets = resourceSetService.getAllForOwnerAndClient(owner, o2a.getOAuth2Request().getClientId()); + } else { + // otherwise get everything for the current user + resourceSets = resourceSetService.getAllForOwner(owner); + } + + // build the entity here and send to the display + + Set ids = new HashSet<>(); + for (ResourceSet resourceSet : resourceSets) { + ids.add(resourceSet.getId().toString()); // add them all as strings so that gson renders them properly + } + + m.addAttribute(JsonEntityView.ENTITY, ids); + return JsonEntityView.VIEWNAME; + } + + private ResourceSet parseResourceSet(String jsonString) { + + try { + JsonElement el = parser.parse(jsonString); + + if (el.isJsonObject()) { + JsonObject o = el.getAsJsonObject(); + + ResourceSet rs = new ResourceSet(); + rs.setId(getAsLong(o, "_id")); + rs.setName(getAsString(o, "name")); + rs.setIconUri(getAsString(o, "icon_uri")); + rs.setType(getAsString(o, "type")); + rs.setScopes(getAsStringSet(o, "scopes")); + rs.setUri(getAsString(o, "uri")); + + return rs; + + } + + return null; + + } catch (JsonParseException e) { + return null; + } + + } + + + /** + * + * Make sure the resource set doesn't have any restricted or reserved scopes. + * + * @param rs + */ + private ResourceSet validateScopes(ResourceSet rs) { + // scopes that the client is asking for + Set requestedScopes = scopeService.fromStrings(rs.getScopes()); + + // the scopes that the resource set can have must be a subset of the dynamically allowed scopes + Set allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes); + + rs.setScopes(scopeService.toStrings(allowedScopes)); + + return rs; + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/web/UmaDiscoveryEndpoint.java b/uma-server/src/main/java/org/mitre/uma/web/UmaDiscoveryEndpoint.java new file mode 100644 index 0000000000..6dc8717adb --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/web/UmaDiscoveryEndpoint.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.web; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.mitre.oauth2.web.IntrospectionEndpoint; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.web.DynamicClientRegistrationEndpoint; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; + +/** + * @author jricher + * + */ +@Controller +public class UmaDiscoveryEndpoint { + + @Autowired + private ConfigurationPropertiesBean config; + + @RequestMapping(".well-known/uma-configuration") + public String umaConfiguration(Model model) { + + Map m = new HashMap<>(); + + String issuer = config.getIssuer(); + ImmutableSet tokenProfiles = ImmutableSet.of("bearer"); + ArrayList grantTypes = Lists.newArrayList("authorization_code", "implicit", "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_credentials", "urn:ietf:params:oauth:grant_type:redelegate"); + + m.put("version", "1.0"); + m.put("issuer", issuer); + m.put("pat_profiles_supported", tokenProfiles); + m.put("aat_profiles_supported", tokenProfiles); + m.put("rpt_profiles_supported", tokenProfiles); + m.put("pat_grant_types_supported", grantTypes); + m.put("aat_grant_types_supported", grantTypes); + m.put("claim_token_profiles_supported", ImmutableSet.of()); + m.put("uma_profiles_supported", ImmutableSet.of()); + m.put("dynamic_client_endpoint", issuer + DynamicClientRegistrationEndpoint.URL); + m.put("token_endpoint", issuer + "token"); + m.put("authorization_endpoint", issuer + "authorize"); + m.put("requesting_party_claims_endpoint", issuer + ClaimsCollectionEndpoint.URL); + m.put("introspection_endpoint", issuer + IntrospectionEndpoint.URL); + m.put("resource_set_registration_endpoint", issuer + ResourceSetRegistrationEndpoint.DISCOVERY_URL); + m.put("permission_registration_endpoint", issuer + PermissionRegistrationEndpoint.URL); + m.put("rpt_endpoint", issuer + AuthorizationRequestEndpoint.URL); + + + + model.addAttribute("entity", m); + return JsonEntityView.VIEWNAME; + } + + +} diff --git a/uma-server/src/main/java/org/mitre/uma/web/UserClaimSearchHelper.java b/uma-server/src/main/java/org/mitre/uma/web/UserClaimSearchHelper.java new file mode 100644 index 0000000000..3773264707 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/web/UserClaimSearchHelper.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.web; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.mitre.openid.connect.client.model.IssuerServiceResponse; +import org.mitre.openid.connect.client.service.impl.WebfingerIssuerService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.model.UserInfo; +import org.mitre.openid.connect.service.UserInfoService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.openid.connect.web.RootController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import com.google.common.collect.ImmutableSet; + + +/** + * @author jricher + * + */ +@Controller +@RequestMapping("/" + UserClaimSearchHelper.URL) +@PreAuthorize("hasRole('ROLE_USER')") +public class UserClaimSearchHelper { + + public static final String URL = RootController.API_URL + "/emailsearch"; + + private WebfingerIssuerService webfingerIssuerService = new WebfingerIssuerService(); + + @Autowired + private UserInfoService userInfoService; + + @Autowired + private ConfigurationPropertiesBean config; + + + @RequestMapping(method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + public String search(@RequestParam(value = "identifier") String email, Model m, Authentication auth, HttpServletRequest req) { + + // check locally first + UserInfo localUser = userInfoService.getByEmailAddress(email); + + if (localUser != null) { + Map e = new HashMap<>(); + e.put("issuer", ImmutableSet.of(config.getIssuer())); + e.put("name", "email"); + e.put("value", localUser.getEmail()); + + Map ev = new HashMap<>(); + ev.put("issuer", ImmutableSet.of(config.getIssuer())); + ev.put("name", "email_verified"); + ev.put("value", localUser.getEmailVerified()); + + Map s = new HashMap<>(); + s.put("issuer", ImmutableSet.of(config.getIssuer())); + s.put("name", "sub"); + s.put("value", localUser.getSub()); + + m.addAttribute(JsonEntityView.ENTITY, ImmutableSet.of(e, ev, s)); + return JsonEntityView.VIEWNAME; + } else { + + // otherwise do a webfinger lookup + IssuerServiceResponse resp = webfingerIssuerService.getIssuer(req); + + if (resp != null && resp.getIssuer() != null) { + // we found an issuer, return that + Map e = new HashMap<>(); + e.put("issuer", ImmutableSet.of(resp.getIssuer())); + e.put("name", "email"); + e.put("value", email); + + Map ev = new HashMap<>(); + ev.put("issuer", ImmutableSet.of(resp.getIssuer())); + ev.put("name", "email_verified"); + ev.put("value", true); + + m.addAttribute(JsonEntityView.ENTITY, ImmutableSet.of(e, ev)); + return JsonEntityView.VIEWNAME; + } else { + m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); + return JsonErrorView.VIEWNAME; + } + } + } + +} diff --git a/uma-server/src/test/java/org/mitre/uma/service/impl/TestDefaultPermissionService.java b/uma-server/src/test/java/org/mitre/uma/service/impl/TestDefaultPermissionService.java new file mode 100644 index 0000000000..0a2063cb3e --- /dev/null +++ b/uma-server/src/test/java/org/mitre/uma/service/impl/TestDefaultPermissionService.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import static org.mockito.Matchers.anySetOf; + +import java.util.Set; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.service.SystemScopeService; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.repository.PermissionRepository; +import org.mockito.AdditionalAnswers; +import org.mockito.InjectMocks; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException; + +import com.google.common.collect.ImmutableSet; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; + +import static org.mockito.Mockito.when; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author jricher + * + */ +@RunWith(MockitoJUnitRunner.class) +public class TestDefaultPermissionService { + + @Mock + private PermissionRepository permissionRepository; + + @Mock + private SystemScopeService scopeService; + + @InjectMocks + private DefaultPermissionService permissionService; + + private Set scopes1 = ImmutableSet.of("foo", "bar", "baz"); + private Set scopes2 = ImmutableSet.of("alpha", "beta", "betest"); + + private ResourceSet rs1; + private ResourceSet rs2; + + private String rs1Name = "resource set 1"; + private String rs1Owner = "resource set owner 1"; + private Long rs1Id = 1L; + + private String rs2Name = "resource set 2"; + private String rs2Owner = "resource set owner 2"; + private Long rs2Id = 2L; + + + @Before + public void prepare() { + rs1 = new ResourceSet(); + rs1.setName(rs1Name); + rs1.setOwner(rs1Owner); + rs1.setId(rs1Id ); + rs1.setScopes(scopes1); + + rs2 = new ResourceSet(); + rs2.setName(rs2Name); + rs2.setOwner(rs2Owner); + rs2.setId(rs2Id); + rs2.setScopes(scopes2); + + // have the repository just pass the argument through + when(permissionRepository.save(Matchers.any(PermissionTicket.class))).then(AdditionalAnswers.returnsFirstArg()); + + when(scopeService.scopesMatch(anySetOf(String.class), anySetOf(String.class))).then(new Answer() { + + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + @SuppressWarnings("unchecked") + Set expected = (Set) arguments[0]; + @SuppressWarnings("unchecked") + Set actual = (Set) arguments[1]; + + return expected.containsAll(actual); + } + }); + + } + + + /** + * Test method for {@link org.mitre.uma.service.impl.DefaultPermissionService#createTicket(org.mitre.uma.model.ResourceSet, java.util.Set)}. + */ + @Test + public void testCreate_ticket() { + + PermissionTicket perm = permissionService.createTicket(rs1, scopes1); + + // we want there to be a non-null ticket + assertNotNull(perm.getTicket()); + } + + @Test + public void testCreate_uuid() { + PermissionTicket perm = permissionService.createTicket(rs1, scopes1); + + // we expect this to be a UUID + UUID uuid = UUID.fromString(perm.getTicket()); + + assertNotNull(uuid); + + } + + @Test + public void testCreate_differentTicketsSameClient() { + + PermissionTicket perm1 = permissionService.createTicket(rs1, scopes1); + PermissionTicket perm2 = permissionService.createTicket(rs1, scopes1); + + assertNotNull(perm1.getTicket()); + assertNotNull(perm2.getTicket()); + + // make sure these are different from each other + assertThat(perm1.getTicket(), not(equalTo(perm2.getTicket()))); + + } + + @Test + public void testCreate_differentTicketsDifferentClient() { + + PermissionTicket perm1 = permissionService.createTicket(rs1, scopes1); + PermissionTicket perm2 = permissionService.createTicket(rs2, scopes2); + + assertNotNull(perm1.getTicket()); + assertNotNull(perm2.getTicket()); + + // make sure these are different from each other + assertThat(perm1.getTicket(), not(equalTo(perm2.getTicket()))); + + } + + @Test(expected = InsufficientScopeException.class) + public void testCreate_scopeMismatch() { + @SuppressWarnings("unused") + // try to get scopes outside of what we're allowed to do, this should throw an exception + PermissionTicket perm = permissionService.createTicket(rs1, scopes2); + } + +} diff --git a/uma-server/src/test/java/org/mitre/uma/service/impl/TestDefaultResourceSetService.java b/uma-server/src/test/java/org/mitre/uma/service/impl/TestDefaultResourceSetService.java new file mode 100644 index 0000000000..52ca091be8 --- /dev/null +++ b/uma-server/src/test/java/org/mitre/uma/service/impl/TestDefaultResourceSetService.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright 2018 The MIT Internet Trust Consortium + * + * 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 org.mitre.uma.service.impl; + +import static org.mockito.Matchers.any; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.repository.ResourceSetRepository; +import org.mockito.AdditionalAnswers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Mockito.when; + +/** + * @author jricher + * + */ +@RunWith(MockitoJUnitRunner.class) +public class TestDefaultResourceSetService { + + @Mock + private ResourceSetRepository repository; + + @InjectMocks + private DefaultResourceSetService resourceSetService; + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + + when(repository.save(any(ResourceSet.class))).then(AdditionalAnswers.returnsFirstArg()); + + } + + /** + * Test method for {@link org.mitre.uma.service.impl.DefaultResourceSetService#saveNew(org.mitre.uma.model.ResourceSet)}. + */ + @Test(expected = IllegalArgumentException.class) + public void testSaveNew_hasId() { + + ResourceSet rs = new ResourceSet(); + rs.setId(1L); + + resourceSetService.saveNew(rs); + + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdate_nullId() { + ResourceSet rs = new ResourceSet(); + rs.setId(1L); + + ResourceSet rs2 = new ResourceSet(); + + resourceSetService.update(rs, rs2); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdate_nullId2() { + ResourceSet rs = new ResourceSet(); + + ResourceSet rs2 = new ResourceSet(); + rs2.setId(1L); + + resourceSetService.update(rs, rs2); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdate_mismatchedIds() { + ResourceSet rs = new ResourceSet(); + rs.setId(1L); + + ResourceSet rs2 = new ResourceSet(); + rs2.setId(2L); + + resourceSetService.update(rs, rs2); + + } + +}