Skip to content

Commit

Permalink
feat(jans-auth-server): added token exchange grant
Browse files Browse the repository at this point in the history
Native SSO

#2518
  • Loading branch information
yuriyz committed Oct 27, 2022
1 parent cc206be commit 1402894
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private Constants() {
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
public static final String SUBJECT_TOKEN_TYPE_ID_TOKEN = "urn:ietf:params:oauth:token-type:id_token";
public static final String ACTOR_TOKEN_TYPE_DEVICE_SECRET = "urn:x-oath:params:oauth:token-type:device-secret";
public static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";

public static final String CONTENT_TYPE_APPLICATION_JSON_UTF_8 = "application/json;charset=UTF-8";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ public class AppConfiguration implements Configuration {
private Set<AuthorizationRequestCustomParameter> authorizationRequestCustomAllowedParameters;
private Boolean openidScopeBackwardCompatibility = false;
private Boolean disableU2fEndpoint = false;
private Boolean rotateDeviceSecret = false;

private Boolean dcrSignatureValidationEnabled = false;
private String dcrSignatureValidationSharedSecret;
Expand Down Expand Up @@ -345,6 +346,15 @@ public void setAllowAllValueForRevokeEndpoint(Boolean allowAllValueForRevokeEndp
this.allowAllValueForRevokeEndpoint = allowAllValueForRevokeEndpoint;
}

public Boolean getRotateDeviceSecret() {
if (rotateDeviceSecret == null) rotateDeviceSecret = false;
return rotateDeviceSecret;
}

public void setRotateDeviceSecret(Boolean rotateDeviceSecret) {
this.rotateDeviceSecret = rotateDeviceSecret;
}

public Boolean getRequirePkce() {
if (requirePkce == null) requirePkce = false;
return requirePkce;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public void addDeviceSecretToSession(AuthzRequest authzRequest, SessionId sessio
return;
}

final String newDeviceSecret = HandleTokenFactory.generateHandleToken();
final String newDeviceSecret = HandleTokenFactory.generateDeviceSecret();

final List<String> deviceSecrets = sessionId.getDeviceSecrets();
deviceSecrets.add(newDeviceSecret);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ public ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredenti
return grant;
}

@Override
public TokenExchangeGrant createTokenExchangeGrant(User user, Client client) {
TokenExchangeGrant grant = grantInstance.select(TokenExchangeGrant.class).get();
grant.init(user, client);
return grant;
}

@Override
public CIBAGrant createCIBAGrant(CibaRequestCacheControl request) {
CIBAGrant grant = grantInstance.select(CIBAGrant.class).get();
Expand Down Expand Up @@ -308,6 +315,12 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) {

result = deviceCodeGrant;
break;
case TOKEN_EXCHANGE:
TokenExchangeGrant tokenExchangeGrant = grantInstance.select(TokenExchangeGrant.class).get();
tokenExchangeGrant.init(user, AuthorizationGrantType.TOKEN_EXCHANGE, client, tokenEntity.getCreationDate());

result = tokenExchangeGrant;
break;
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public enum AuthorizationGrantType implements HasParamName {
* grant types are not available (such as an authorization code).
*/
RESOURCE_OWNER_PASSWORD_CREDENTIALS("resource_owner_password_credentials"),

TOKEN_EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange"),
/**
* An extension grant for Client Initiated Backchannel Authentication.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public interface IAuthorizationGrantList {

ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredentialsGrant(User user, Client client);

TokenExchangeGrant createTokenExchangeGrant(User user, Client client);

CIBAGrant createCIBAGrant(CibaRequestCacheControl request);

AuthorizationCodeGrant getAuthorizationCodeGrant(String authorizationCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.jans.as.server.model.common;

import io.jans.as.common.model.common.User;
import io.jans.as.common.model.registration.Client;
import io.jans.as.model.common.GrantType;

/**
* @author Yuriy Z
*/
public class TokenExchangeGrant extends AuthorizationGrant {

public TokenExchangeGrant() {
}

public void init(User user, Client client) {
super.init(user, AuthorizationGrantType.TOKEN_EXCHANGE, client, null);
}

@Override
public GrantType getGrantType() {
return GrantType.TOKEN_EXCHANGE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ public class HandleTokenFactory {
public static String generateHandleToken() {
return UUID.randomUUID().toString();
}

public static String generateDeviceSecret() {
return UUID.randomUUID().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.jans.as.server.token.ws.rs;

import io.jans.as.common.model.session.SessionId;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.server.model.token.HandleTokenFactory;
import io.jans.as.server.service.SessionIdService;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger;

import java.util.List;

/**
* @author Yuriy Z
*/
@Stateless
@Named
public class TokenInternalService {

@Inject
private Logger log;

@Inject
private AppConfiguration appConfiguration;

@Inject
private SessionIdService sessionIdService;

public String rotateDeviceSecret(SessionId sessionId, String actorToken) {
if (BooleanUtils.isFalse(appConfiguration.getRotateDeviceSecret())) {
return null;
}

String newDeviceSecret = HandleTokenFactory.generateDeviceSecret();

final List<String> deviceSecrets = sessionId.getDeviceSecrets();
deviceSecrets.remove(actorToken);
deviceSecrets.add(newDeviceSecret);

sessionIdService.updateSessionId(sessionId, false);

return newDeviceSecret;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import java.util.Date;
import java.util.function.Function;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.jans.as.model.config.Constants.*;
import static org.apache.commons.lang.BooleanUtils.isFalse;
import static org.apache.commons.lang.BooleanUtils.isTrue;
Expand Down Expand Up @@ -134,6 +135,9 @@ public class TokenRestWebServiceImpl implements TokenRestWebService {
@Inject
private TokenRestWebServiceValidator tokenRestWebServiceValidator;

@Inject
private TokenInternalService tokenInternalService;

@Override
public Response requestAccessToken(String grantType, String code,
String redirectUri, String username, String password, String scope,
Expand Down Expand Up @@ -201,7 +205,7 @@ public Response requestAccessToken(String grantType, String code,
} else if (gt == GrantType.DEVICE_CODE) {
return processDeviceCodeGrantType(executionContext, deviceCode, scope);
} else if (gt == GrantType.TOKEN_EXCHANGE) {
return processTokenExchange(scope, executionContext);
return processTokenExchange(scope, idTokenPreProcessing, executionContext);
}
} catch (WebApplicationException e) {
throw e;
Expand All @@ -213,19 +217,70 @@ public Response requestAccessToken(String grantType, String code,
throw new WebApplicationException(tokenRestWebServiceValidator.error(400, TokenErrorResponseType.UNSUPPORTED_GRANT_TYPE, "Unsupported Grant Type.").build());
}

private Response processTokenExchange(String scope, ExecutionContext executionContext) {
private Response processTokenExchange(String scope, Function<JsonWebResponse, Void> idTokenPreProcessing, ExecutionContext executionContext) {
final HttpServletRequest httpRequest = executionContext.getHttpRequest();
final Client client = executionContext.getClient();
final OAuth2AuditLog auditLog = executionContext.getAuditLog();

final String audience = httpRequest.getParameter("audience");
final String subjectToken = httpRequest.getParameter("subject_token");
final String subjectTokenType = httpRequest.getParameter("subject_token_type");
final String actorToken = httpRequest.getParameter("actor_token");
final String actorTokenType = httpRequest.getParameter("actor_token_type");

tokenRestWebServiceValidator.validateAudience(audience, auditLog);
tokenRestWebServiceValidator.validateSubjectTokenType(subjectTokenType, auditLog);
tokenRestWebServiceValidator.validateActorTokenType(actorTokenType, auditLog);
tokenRestWebServiceValidator.validateActorToken(actorToken, auditLog);

final SessionId sessionId = sessionIdService.getSessionByDeviceSecret(actorToken);
tokenRestWebServiceValidator.validateSessionForTokenExchange(sessionId, actorToken, auditLog);
checkNotNull(sessionId); // it checked by validator, added it only to relax static bugs checker

tokenRestWebServiceValidator.validateSubjectToken(subjectToken, sessionId, auditLog);

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

executionContext.setGrant(tokenExchangeGrant);

scope = tokenExchangeGrant.checkScopesPolicy(scope);

AccessToken accessToken = tokenExchangeGrant.createAccessToken(executionContext); // create token after scopes are checked

IdToken idToken = null;
if (isTrue(appConfiguration.getOpenidScopeBackwardCompatibility()) && tokenExchangeGrant.getScopes().contains(OPENID)) {
boolean includeIdTokenClaims = Boolean.TRUE.equals(
appConfiguration.getLegacyIdTokenClaims());

ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(httpRequest, tokenExchangeGrant, client, appConfiguration, attributeService);

executionContext.setIncludeIdTokenClaims(includeIdTokenClaims);
executionContext.setPreProcessing(idTokenPreProcessing);
executionContext.setPostProcessor(externalUpdateTokenService.buildModifyIdTokenProcessor(context));

idToken = tokenExchangeGrant.createIdToken(
null, null, null, null, null, executionContext);
}

RefreshToken reToken = createRefreshToken(executionContext, scope);

executionContext.getAuditLog().updateOAuth2AuditLog(tokenExchangeGrant, true);

String rotatedDeviceSecret = tokenInternalService.rotateDeviceSecret(sessionId, actorToken);

String audience = httpRequest.getParameter("audience");
String subjectToken = httpRequest.getParameter("subject_token");
String subjectTokenType = httpRequest.getParameter("subject_token_type");
String actorToken = httpRequest.getParameter("actor_token");
String actorTokenType = httpRequest.getParameter("actor_token_type");
JSONObject jsonObj = new JSONObject();
try {
fillJsonObject(jsonObj, accessToken, accessToken.getTokenType(), accessToken.getExpiresIn(), reToken, scope, idToken);
jsonObj.put("issued_token_type", TOKEN_TYPE_ACCESS_TOKEN);
if (StringUtils.isNotBlank(rotatedDeviceSecret)) {
jsonObj.put("device_secret", rotatedDeviceSecret);
}
} catch (JSONException e) {
log.error(e.getMessage(), e);
}

tokenRestWebServiceValidator.validateSubjectTokenType(subjectTokenType, executionContext.getAuditLog());
tokenRestWebServiceValidator.validateActorTokenType(actorTokenType, executionContext.getAuditLog());
// todo
return null;
return response(Response.ok().entity(jsonObj.toString()), executionContext.getAuditLog());
}

private Response processROPC(String username, String password, String scope, GrantType gt, Function<JsonWebResponse, Void> idTokenPreProcessing, ExecutionContext executionContext) throws SearchException {
Expand Down Expand Up @@ -649,27 +704,33 @@ public String getJSonResponse(AccessToken accessToken, TokenType tokenType,
IdToken idToken) {
JSONObject jsonObj = new JSONObject();
try {
jsonObj.put("access_token", accessToken.getCode()); // Required
jsonObj.put("token_type", tokenType.toString()); // Required
if (expiresIn != null) { // Optional
jsonObj.put("expires_in", expiresIn);
}
if (refreshToken != null) { // Optional
jsonObj.put("refresh_token", refreshToken.getCode());
}
if (scope != null) { // Optional
jsonObj.put("scope", scope);
}
if (idToken != null) {
jsonObj.put("id_token", idToken.getCode());
}
fillJsonObject(jsonObj, accessToken, tokenType, expiresIn, refreshToken, scope, idToken);
} catch (JSONException e) {
log.error(e.getMessage(), e);
}

return jsonObj.toString();
}

public void fillJsonObject(JSONObject jsonObj, AccessToken accessToken, TokenType tokenType,
Integer expiresIn, RefreshToken refreshToken, String scope,
IdToken idToken) {
jsonObj.put("access_token", accessToken.getCode()); // Required
jsonObj.put("token_type", tokenType.toString()); // Required
if (expiresIn != null) { // Optional
jsonObj.put("expires_in", expiresIn);
}
if (refreshToken != null) { // Optional
jsonObj.put("refresh_token", refreshToken.getCode());
}
if (scope != null) { // Optional
jsonObj.put("scope", scope);
}
if (idToken != null) {
jsonObj.put("id_token", idToken.getCode());
}
}

private Response processCIBA(String scope, String authReqId, Function<JsonWebResponse, Void> idTokenPreProcessing, ExecutionContext executionContext) {
errorResponseFactory.validateFeatureEnabled(FeatureFlagType.CIBA);

Expand Down
Loading

0 comments on commit 1402894

Please sign in to comment.