Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jans-auth-server): added token exchange support to client #2518 #2855

Merged
merged 1 commit into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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