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

fix(jans-auth-server): native sso - return device secret if device_sso scope is present #2790 #2791

Merged
merged 1 commit into from
Oct 31, 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 @@ -30,6 +30,7 @@ public class TokenResponse extends BaseResponseWithErrors<TokenErrorResponseType
private String refreshToken;
private String scope;
private String idToken;
private String deviceToken;

public TokenResponse() {
}
Expand Down Expand Up @@ -75,12 +76,20 @@ public void injectDataFromJson(String json) {
if (jsonObj.has("id_token")) {
setIdToken(jsonObj.getString("id_token"));
}
setDeviceToken(jsonObj.optString("device_token"));
} catch (JSONException e) {
LOG.error(e.getMessage(), e);
}
}
}

public String getDeviceToken() {
return deviceToken;
}

public void setDeviceToken(String deviceToken) {
this.deviceToken = deviceToken;
}

/**
* Returns the access token issued by the authorization server.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.jans.as.client.OpenIdConfigurationResponse;
import io.jans.as.client.RegisterResponse;
import io.jans.as.model.register.RegisterRequestParam;
import org.apache.commons.lang.StringUtils;

import java.util.Arrays;

Expand All @@ -24,6 +25,22 @@ public class Asserter {
private Asserter() {
}

public static void assertBlank(String value) {
assertTrue(StringUtils.isNotBlank(value));
}

public static void assertBlank(String value, String message) {
assertTrue(StringUtils.isBlank(value), message);
}

public static void assertNotBlank(String value) {
assertTrue(StringUtils.isNotBlank(value));
}

public static void assertNotBlank(String value, String message) {
assertTrue(StringUtils.isNotBlank(value), message);
}

public static void assertRegisterResponseClaimsNotNull(RegisterResponse response, RegisterRequestParam... claimsToVerify) {
if (response == null || claimsToVerify == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.List;

import static io.jans.as.client.BaseTest.clientEngine;
import static io.jans.as.client.client.Asserter.assertNotBlank;
import static org.testng.Assert.*;

public class JwtAssertBuilder extends BaseAssertBuilder {
Expand All @@ -28,6 +29,7 @@ public class JwtAssertBuilder extends BaseAssertBuilder {
private boolean notNullAuthenticationMethodReferences;
private boolean notNullClaimsAddressdata;
private boolean checkMemberOfClaimNoEmpty;
private boolean notBlankDsHash;
private String[] claimsPresence;
private String[] claimsNoPresence;

Expand All @@ -53,6 +55,11 @@ public JwtAssertBuilder notNullAccesTokenHash() {
return this;
}

public JwtAssertBuilder notBlankDsHash() {
notBlankDsHash = true;
return this;
}

public JwtAssertBuilder notNullAuthenticationTime() {
this.notNullAuthenticationTime = true;
return this;
Expand Down Expand Up @@ -181,6 +188,10 @@ public void check() {
assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1);
}

if (notBlankDsHash) {
assertNotBlank(jwt.getClaims().getClaimAsString("ds_hash"), "ds_hash claim is not present");
}

if (notNullClaimsAddressdata) {
assertNotNullClaim(JwtClaimName.ADDRESS_STREET_ADDRESS);
assertNotNullClaim(JwtClaimName.ADDRESS_COUNTRY);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.jans.as.client.client.assertbuilders;

import io.jans.as.client.TokenResponse;
import io.jans.as.client.client.AssertBuilder;
import io.jans.as.model.token.TokenErrorResponseType;

import static io.jans.as.client.client.Asserter.assertNotBlank;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

Expand All @@ -12,6 +12,7 @@ public class TokenResponseAssertBuilder extends BaseAssertBuilder {
private TokenResponse response;
private int status = 200;
private boolean notNullRefreshToken;
private boolean notBlankDeviceToken;
private boolean notNullIdToken;
private boolean notNullScope;
private boolean nullRefreshToken;
Expand Down Expand Up @@ -53,6 +54,11 @@ public TokenResponseAssertBuilder notNullRefreshToken() {
return this;
}

public TokenResponseAssertBuilder notBlankDeviceToken() {
this.notBlankDeviceToken = true;
return this;
}

public TokenResponseAssertBuilder notNullIdToken() {
this.notNullIdToken = true;
return this;
Expand Down Expand Up @@ -89,6 +95,9 @@ public void check() {
if (notNullRefreshToken) {
assertNotNull(response.getRefreshToken(), "The refresh token is null");
}
if (notBlankDeviceToken) {
assertNotBlank(response.getDeviceToken(), "The device token is blank");
}
} else {
assertEquals(response.getStatus(), status, "Unexpected HTTP status response: " + response.getEntity());
assertNotNull(response.getEntity(), "The entity is null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class ExecutionContext {

private String dpop;
private String certAsPem;
private String deviceSecret;

private String nonce;
private String state;
Expand All @@ -71,6 +72,14 @@ public ExecutionContext(HttpServletRequest httpRequest, HttpServletResponse http
this.httpResponse = httpResponse;
}

public String getDeviceSecret() {
return deviceSecret;
}

public void setDeviceSecret(String deviceSecret) {
this.deviceSecret = deviceSecret;
}

@NotNull
public Map<String, String> getAttributes() {
return attributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,11 @@ private void addTokenExchangeClaims(JsonWebResponse jwr, ExecutionContext execut
return;
}

String deviceSecret = executionContext.getHttpRequest().getParameter(DEVICE_SECRET);
String deviceSecret = executionContext.getDeviceSecret();
if (StringUtils.isBlank(deviceSecret)) {
deviceSecret = executionContext.getHttpRequest().getParameter(DEVICE_SECRET);
}

if (StringUtils.isNotBlank(deviceSecret) && sessionId.getDeviceSecrets().contains(deviceSecret)) {
jwr.setClaim("ds_hash", CodeVerifier.s256(deviceSecret));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,30 +172,32 @@ public JSONObject processTokenExchange(String scope, Function<JsonWebResponse, V
return jsonObj;
}

public void putNewDeviceSecret(JSONObject jsonObj, String sessionDn, Client client, String scope) {
public String createNewDeviceSecret(String sessionDn, Client client, String scope) {
if (StringUtils.isBlank(scope) || !scope.contains(ScopeConstants.DEVICE_SSO)) {
return;
log.debug("Skip device secret. No device_sso scope.");
return null;
}
if (client == null || !ArrayUtils.contains(client.getGrantTypes(), GrantType.TOKEN_EXCHANGE)) {
log.debug("Skip device secret. Scope has {} value but client does not have Token Exchange Grant Type enabled ('urn:ietf:params:oauth:grant-type:token-exchange')", ScopeConstants.DEVICE_SSO);
return;
return null;
}

try {
final SessionId sessionId = sessionIdService.getSessionByDn(sessionDn);
if (sessionId == null) {
log.debug("Unable to find session by dn: {}", sessionDn);
return;
return null;
}

String newDeviceSecret = HandleTokenFactory.generateDeviceSecret();
sessionId.getDeviceSecrets().add(newDeviceSecret);

sessionIdService.updateSessionId(sessionId, false);

jsonObj.put("device_token", newDeviceSecret);
return newDeviceSecret;
} catch (Exception e) {
log.error("Failed to generate device_secret", e);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ private Response processAuthorizationCode(String code, String scope, String code
scope = authorizationCodeGrant.checkScopesPolicy(scope);

AccessToken accToken = authorizationCodeGrant.createAccessToken(executionContext); // create token after scopes are checked
final String deviceSecret = tokenExchangeService.createNewDeviceSecret(authorizationCodeGrant.getSessionDn(), client, authorizationCodeGrant.getScopesAsString());

IdToken idToken = null;
if (authorizationCodeGrant.getScopes().contains(OPENID)) {
Expand All @@ -414,6 +415,7 @@ private Response processAuthorizationCode(String code, String scope, String code

ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(executionContext.getHttpRequest(), authorizationCodeGrant, client, appConfiguration, attributeService);

executionContext.setDeviceSecret(deviceSecret);
executionContext.setIncludeIdTokenClaims(includeIdTokenClaims);
executionContext.setPreProcessing(JwrService.wrapWithSidFunction(authorizationCodePreProcessing, sessionIdObj != null ? sessionIdObj.getOutsideSid() : null));
executionContext.setPostProcessor(externalUpdateTokenService.buildModifyIdTokenProcessor(context));
Expand All @@ -429,7 +431,9 @@ private Response processAuthorizationCode(String code, String scope, String code
JSONObject jsonObj = new JSONObject();
try {
fillJsonObject(jsonObj, accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken);
tokenExchangeService.putNewDeviceSecret(jsonObj, authorizationCodeGrant.getSessionDn(), client, scope);
if (StringUtils.isNotBlank(deviceSecret)) {
jsonObj.put("device_token", deviceSecret);
}
} catch (JSONException e) {
log.error(e.getMessage(), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
import io.jans.as.server.audit.ApplicationAuditLogger;
import io.jans.as.server.service.SessionIdService;
import org.apache.commons.lang.StringUtils;
import org.json.JSONObject;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.slf4j.Logger;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

import static io.jans.as.server.util.TestUtil.assertEmpty;
import static io.jans.as.server.util.TestUtil.assertNotEmpty;
import static org.mockito.Mockito.when;
import static org.testng.Assert.*;

Expand Down Expand Up @@ -48,56 +49,53 @@ public class TokenExchangeServiceTest {
private TokenExchangeService tokenExchangeService;

@Test
public void putNewDeviceSecret_whenScopeIsNull_shouldNotGenerateDeviceSecretAndShouldNotThrowNPE() {
public void createNewDeviceSecret_whenScopeIsNull_shouldNotGenerateDeviceSecretAndShouldNotThrowNPE() {
SessionId sessionId = new SessionId();
Client client = new Client();
client.setGrantTypes(new GrantType[] {GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE});
client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE});

final JSONObject jsonObj = new JSONObject();
tokenExchangeService.putNewDeviceSecret(jsonObj, "sessionDn", client, null);
String deviceSecret = tokenExchangeService.createNewDeviceSecret("sessionDn", client, null);

assertEmpty(deviceSecret);
assertTrue(sessionId.getDeviceSecrets().isEmpty());
assertFalse(jsonObj.has("device_token"));
}

@Test
public void putNewDeviceSecret_whenScopeDeviceSSOIsNotPresent_shouldNotGenerateDeviceSecret() {
public void createNewDeviceSecret_whenScopeDeviceSSOIsNotPresent_shouldNotGenerateDeviceSecret() {
SessionId sessionId = new SessionId();
Client client = new Client();
client.setGrantTypes(new GrantType[] {GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE});
client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE});

final JSONObject jsonObj = new JSONObject();
tokenExchangeService.putNewDeviceSecret(jsonObj, "sessionDn", client, "openid");
final String deviceSecret = tokenExchangeService.createNewDeviceSecret("sessionDn", client, "openid");

assertEmpty(deviceSecret);
assertTrue(sessionId.getDeviceSecrets().isEmpty());
assertFalse(jsonObj.has("device_token"));
}

@Test
public void putNewDeviceSecret_whenTokenExchangeGrantIsNotPresent_shouldNotGenerateDeviceSecret() {
public void createNewDeviceSecret_whenTokenExchangeGrantIsNotPresent_shouldNotGenerateDeviceSecret() {
SessionId sessionId = new SessionId();
Client client = new Client();
client.setGrantTypes(new GrantType[] {GrantType.AUTHORIZATION_CODE});
client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE});

final JSONObject jsonObj = new JSONObject();
tokenExchangeService.putNewDeviceSecret(jsonObj, "sessionDn", client, "openid device_sso");
final String deviceSecret = tokenExchangeService.createNewDeviceSecret("sessionDn", client, "openid device_sso");

assertEmpty(deviceSecret);
assertTrue(sessionId.getDeviceSecrets().isEmpty());
assertFalse(jsonObj.has("device_token"));
}

@Test
public void putNewDeviceSecret_whenAllConditionsMet_shouldGenerateDeviceSecret() {
public void createNewDeviceSecret_whenAllConditionsMet_shouldGenerateDeviceSecret() {
SessionId sessionId = new SessionId();
Client client = new Client();
client.setGrantTypes(new GrantType[] {GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE});
client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE});

when(sessionIdService.getSessionByDn("sessionDn")).thenReturn(sessionId);

final JSONObject jsonObj = new JSONObject();
tokenExchangeService.putNewDeviceSecret(jsonObj, "sessionDn", client, "openid device_sso");
final String deviceSecret = tokenExchangeService.createNewDeviceSecret("sessionDn", client, "openid device_sso");

assertTrue(sessionId.getDeviceSecrets().contains(jsonObj.getString("device_token")));
assertNotEmpty(deviceSecret);
assertTrue(sessionId.getDeviceSecrets().contains(deviceSecret));
}

@Test
Expand Down