From 04221b6ed2fc45e52411194ba0e6f33bb598bb9f Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Mon, 7 Nov 2022 11:10:05 +0200 Subject: [PATCH] feat(jans-auth-server): added token exchange support to client #2518 (#2855) And added native sso http test. --- .../java/io/jans/as/client/TokenClient.java | 19 +- .../java/io/jans/as/client/TokenRequest.java | 82 ++++++++ .../client/ws/rs/token/NativeSsoHttpTest.java | 183 ++++++++++++++++++ .../client/src/test/resources/testng.xml | 6 + .../as/model/token/TokenRequestParam.java | 7 + .../token/ws/rs/TokenExchangeService.java | 3 +- .../ws/rs/TokenRestWebServiceValidator.java | 6 +- 7 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/NativeSsoHttpTest.java diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java index 25ea2860b01..e0b75e507ea 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java @@ -14,18 +14,7 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.Invocation.Builder; -import static io.jans.as.model.token.TokenRequestParam.ASSERTION; -import static io.jans.as.model.token.TokenRequestParam.AUTH_REQ_ID; -import static io.jans.as.model.token.TokenRequestParam.CODE; -import static io.jans.as.model.token.TokenRequestParam.CODE_VERIFIER; -import static io.jans.as.model.token.TokenRequestParam.DEVICE_CODE; -import static io.jans.as.model.token.TokenRequestParam.DPOP; -import static io.jans.as.model.token.TokenRequestParam.GRANT_TYPE; -import static io.jans.as.model.token.TokenRequestParam.PASSWORD; -import static io.jans.as.model.token.TokenRequestParam.REDIRECT_URI; -import static io.jans.as.model.token.TokenRequestParam.REFRESH_TOKEN; -import static io.jans.as.model.token.TokenRequestParam.SCOPE; -import static io.jans.as.model.token.TokenRequestParam.USERNAME; +import static io.jans.as.model.token.TokenRequestParam.*; /** * Encapsulates functionality to make token request calls to an authorization @@ -244,6 +233,12 @@ public TokenResponse exec() { addFormParameterIfNotBlank(REFRESH_TOKEN, getRequest().getRefreshToken()); addFormParameterIfNotBlank(AUTH_REQ_ID, getRequest().getAuthReqId()); addFormParameterIfNotBlank(DEVICE_CODE, getRequest().getDeviceCode()); + addFormParameterIfNotBlank(AUDIENCE, getRequest().getAudience()); + addFormParameterIfNotBlank(SUBJECT_TOKEN, getRequest().getSubjectToken()); + addFormParameterIfNotBlank(SUBJECT_TOKEN_TYPE, getRequest().getSubjectTokenType()); + addFormParameterIfNotBlank(ACTOR_TOKEN, getRequest().getActorToken()); + addFormParameterIfNotBlank(ACTOR_TOKEN_TYPE, getRequest().getActorTokenType()); + addFormParameterIfNotBlank(REQUESTED_TOKEN_TYPE, getRequest().getRequestedTokenType()); for (String key : getRequest().getCustomParameters().keySet()) { addFormParameterIfNotBlank(key, getRequest().getCustomParameters().get(key)); diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java index 3a650dfcd02..fbb5d553fa5 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java @@ -14,6 +14,8 @@ import io.jans.as.model.util.QueryBuilder; import jakarta.ws.rs.core.MediaType; +import org.apache.commons.lang.StringUtils; + import java.util.HashMap; import java.util.Map; @@ -36,6 +38,12 @@ public class TokenRequest extends ClientAuthnRequest { private String codeVerifier; private String authReqId; private String deviceCode; + private String audience; + private String subjectToken; + private String subjectTokenType; + private String actorToken; + private String actorTokenType; + private String requestedTokenType; private DPoP dpop; /** @@ -61,6 +69,56 @@ public static Builder umaBuilder() { return new Builder().grantType(GrantType.CLIENT_CREDENTIALS); } + public String getSubjectToken() { + return subjectToken; + } + + public void setSubjectToken(String subjectToken) { + this.subjectToken = subjectToken; + } + + public String getSubjectTokenType() { + return subjectTokenType; + } + + public void setSubjectTokenType(String subjectTokenType) { + this.subjectTokenType = subjectTokenType; + } + + public String getActorToken() { + return actorToken; + } + + public void setActorToken(String actorToken) { + this.actorToken = actorToken; + } + + public String getActorTokenType() { + return actorTokenType; + } + + public void setActorTokenType(String actorTokenType) { + this.actorTokenType = actorTokenType; + } + + public String getRequestedTokenType() { + return requestedTokenType; + } + + public void setRequestedTokenType(String requestedTokenType) { + this.requestedTokenType = requestedTokenType; + } + + @Override + public String getAudience() { + return audience; + } + + @Override + public void setAudience(String audience) { + this.audience = audience; + } + /** * Returns the grant type. * @@ -267,6 +325,12 @@ public String getQueryString() { builder.append("refresh_token", refreshToken); builder.append("auth_req_id", authReqId); builder.append("device_code", deviceCode); + builder.append("audience", audience); + builder.append("subject_token", subjectToken); + builder.append("subject_token_type", subjectTokenType); + builder.append("actor_token", actorToken); + builder.append("actor_token_type", actorTokenType); + builder.append("requested_token_type", requestedTokenType); appendClientAuthnToQuery(builder); for (String key : getCustomParameters().keySet()) { builder.append(key, getCustomParameters().get(key)); @@ -296,6 +360,24 @@ public Map getParameters() { if (username != null && !username.isEmpty()) { parameters.put("username", username); } + if (StringUtils.isNotBlank(audience)) { + parameters.put("audience", audience); + } + if (StringUtils.isNotBlank(subjectToken)) { + parameters.put("subject_token", subjectToken); + } + if (StringUtils.isNotBlank(subjectTokenType)) { + parameters.put("subject_token_type", subjectTokenType); + } + if (StringUtils.isNotBlank(actorToken)) { + parameters.put("actor_token", actorToken); + } + if (StringUtils.isNotBlank(actorTokenType)) { + parameters.put("actor_token_type", actorTokenType); + } + if (StringUtils.isNotBlank(requestedTokenType)) { + parameters.put("requested_token_type", requestedTokenType); + } if (password != null && !password.isEmpty()) { parameters.put("password", password); } diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/NativeSsoHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/NativeSsoHttpTest.java new file mode 100644 index 00000000000..6ee06b2b2c6 --- /dev/null +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/NativeSsoHttpTest.java @@ -0,0 +1,183 @@ +package io.jans.as.client.ws.rs.token; + +import io.jans.as.client.*; +import io.jans.as.client.client.AssertBuilder; +import io.jans.as.model.common.AuthenticationMethod; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.common.ResponseType; +import io.jans.as.model.crypto.signature.SignatureAlgorithm; +import io.jans.as.model.exception.InvalidJwtException; +import io.jans.as.model.jwt.JwtClaimName; +import io.jans.util.Pair; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.testng.Assert.assertTrue; + +/** + * @author Yuriy Z + */ +public class NativeSsoHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void nativeSso( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + final Pair fromApp1 = app1Flow(userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri); + + final String deviceToken = fromApp1.getFirst(); + final String idToken = fromApp1.getSecond(); + + app2Flow(deviceToken, idToken, redirectUris, sectorIdentifierUri); + } + + private Pair app1Flow(String userId, String userSecret, String redirectUris, String redirectUri, String sectorIdentifierUri) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, InvalidJwtException { + showTitle("APP 1 - Perform Authorization Code Flow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE); + List scopes = Arrays.asList("openid", "profile", "device_sso", "email"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + + assertTrue(scope.contains("device_sso")); // make sure device_sso scope is present -> pre-requisite to get device secret + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setScope(""); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = newTokenClient(tokenRequest); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + AssertBuilder.tokenResponse(tokenResponse) + .notNullRefreshToken() + .notBlankDeviceToken() + .check(); + + final String deviceToken = tokenResponse.getDeviceToken(); + + // 4. Validate id_token - ds_hash must be present + final String idToken = tokenResponse.getIdToken(); + AssertBuilder.jwtParse(idToken) + .validateSignatureRSAClientEngine(jwksUri, SignatureAlgorithm.RS256) + .claimsPresence(JwtClaimName.CODE_HASH) + .notNullAuthenticationTime() + .notNullOxOpenIDConnectVersion() + .notNullAuthenticationContextClassReference() + .notNullAuthenticationMethodReferences() + .notBlankDsHash() + .check(); + + String accessToken = tokenResponse.getAccessToken(); + + // 6. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setExecutor(clientEngine(true)); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + AssertBuilder.userInfoResponse(userInfoResponse) + .notNullClaimsPersonalData() + .claimsPresence(JwtClaimName.EMAIL, JwtClaimName.BIRTHDATE, JwtClaimName.GENDER, JwtClaimName.MIDDLE_NAME) + .claimsPresence(JwtClaimName.NICKNAME, JwtClaimName.PREFERRED_USERNAME, JwtClaimName.PROFILE) + .check(); + + return new Pair<>(deviceToken, idToken); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + AssertBuilder.authorizationResponse(authorizationResponse).check(); + return authorizationResponse; + } + + private void app2Flow(String deviceToken, String idToken, String redirectUris, String sectorIdentifierUri) throws InvalidJwtException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + showTitle("APP 2 - Token Exchange with device_token and id_token"); + showTitle("APP 2 gets device_token and id_token via shared secured storage (here we just emulate it.)"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE); + List scopes = Arrays.asList("openid", "profile", "device_sso", "email"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Get access_token by sending id_token and deviceToken + TokenRequest tokenRequest = new TokenRequest(GrantType.TOKEN_EXCHANGE); + tokenRequest.setAudience(issuer); + tokenRequest.setScope("openid profile email"); + tokenRequest.setSubjectToken(idToken); + tokenRequest.setSubjectTokenType("urn:ietf:params:oauth:token-type:id_token"); + tokenRequest.setActorToken(deviceToken); + tokenRequest.setActorTokenType("urn:x-oath:params:oauth:token-type:device-secret"); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = newTokenClient(tokenRequest); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showTitle("APP 2:"); + showClient(tokenClient); + AssertBuilder.tokenResponse(tokenResponse) + .notNullRefreshToken() + .check(); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setExecutor(clientEngine(true)); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + AssertBuilder.userInfoResponse(userInfoResponse) + .notNullClaimsPersonalData() + .claimsPresence(JwtClaimName.EMAIL, JwtClaimName.BIRTHDATE, JwtClaimName.GENDER, JwtClaimName.MIDDLE_NAME) + .claimsPresence(JwtClaimName.NICKNAME, JwtClaimName.PREFERRED_USERNAME, JwtClaimName.PROFILE) + .check(); + } +} diff --git a/jans-auth-server/client/src/test/resources/testng.xml b/jans-auth-server/client/src/test/resources/testng.xml index 3c041b0ffc8..8c72e78444f 100644 --- a/jans-auth-server/client/src/test/resources/testng.xml +++ b/jans-auth-server/client/src/test/resources/testng.xml @@ -22,6 +22,12 @@ + + + + + + diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java b/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java index 0c2a03d3efd..8b660164585 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java @@ -27,6 +27,13 @@ private TokenRequestParam() { public static final String REFRESH_TOKEN = "refresh_token"; public static final String AUTH_REQ_ID = "auth_req_id"; public static final String DEVICE_CODE = "device_code"; + public static final String AUDIENCE = "audience"; + public static final String SUBJECT_TOKEN = "subject_token"; + public static final String SUBJECT_TOKEN_TYPE = "subject_token_type"; + public static final String ACTOR_TOKEN = "actor_token"; + public static final String ACTOR_TOKEN_TYPE = "actor_token_type"; + public static final String REQUESTED_TOKEN_TYPE = "requested_token_type"; + // Demonstrating Proof-of-Possession public static final String DPOP = "DPoP"; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java index d05667158fa..135716dae02 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java @@ -1,6 +1,5 @@ package io.jans.as.server.token.ws.rs; -import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.SessionId; import io.jans.as.common.service.AttributeService; @@ -128,7 +127,7 @@ public JSONObject processTokenExchange(String scope, Function