Skip to content

Commit

Permalink
feat(jans-auth-server): added token exchange support to client #2518 (#…
Browse files Browse the repository at this point in the history
…2855)

And added native sso http test.
  • Loading branch information
yuriyz authored Nov 7, 2022
1 parent 97ab071 commit 943d99f
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

/**
Expand All @@ -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.
*
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -296,6 +360,24 @@ public Map<String, String> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> fromApp1 = app1Flow(userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri);

final String deviceToken = fromApp1.getFirst();
final String idToken = fromApp1.getSecond();

app2Flow(deviceToken, idToken, redirectUris, sectorIdentifierUri);
}

private Pair<String, String> 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<ResponseType> responseTypes = Arrays.asList(
ResponseType.CODE,
ResponseType.ID_TOKEN);
List<GrantType> grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE);
List<String> 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<ResponseType> responseTypes, List<String> 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<ResponseType> responseTypes = Arrays.asList(
ResponseType.CODE,
ResponseType.ID_TOKEN);
List<GrantType> grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE);
List<String> 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();
}
}
6 changes: 6 additions & 0 deletions jans-auth-server/client/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
</classes>
</test>

<test name="Native SSO Client test (HTTP)" enabled="true">
<classes>
<class name="io.jans.as.client.ws.rs.token.NativeSsoHttpTest"/>
</classes>
</test>

<!-- Address claims test -->
<test name="Address claims test (HTTP)" enabled="true">
<classes>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -128,7 +127,7 @@ public JSONObject processTokenExchange(String scope, Function<JsonWebResponse, V

tokenRestWebServiceValidator.validateSubjectToken(deviceSecret, subjectToken, sessionId, auditLog);

TokenExchangeGrant tokenExchangeGrant = authorizationGrantList.createTokenExchangeGrant(new User(), client);
TokenExchangeGrant tokenExchangeGrant = authorizationGrantList.createTokenExchangeGrant(sessionIdService.getUser(sessionId), client);
tokenExchangeGrant.setSessionDn(sessionId.getDn());

executionContext.setGrant(tokenExchangeGrant);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,17 @@ public void validateSessionForTokenExchange(SessionId session, String actorToken
}
}

public void validateSubjectToken(String subjectToken, String deviceSecret, SessionId sidSession, OAuth2AuditLog auditLog) {
public void validateSubjectToken(String deviceSecret, String subjectToken, SessionId sidSession, OAuth2AuditLog auditLog) {
try {
final Jwt jwt = Jwt.parse(subjectToken);
validateSubjectTokenSignature(deviceSecret, sidSession, jwt, auditLog);
} catch (InvalidJwtException e) {
log.error("Unable to parse subject_token as JWT.", e);
log.error("Unable to parse subject_token as JWT, subjectToken: " + subjectToken, e);
throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_REQUEST, "Unable to parse subject_token as JWT."), auditLog));
} catch (WebApplicationException e) {
throw e;
} catch (Exception e) {
log.error("Unable to validate subject_token.", e);
log.error("Unable to validate subject_token, subjectToken: " + subjectToken, e);
throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_REQUEST, "Unable to validate subject_token as JWT."), auditLog));
}
}
Expand Down

0 comments on commit 943d99f

Please sign in to comment.