Skip to content

Commit

Permalink
feat: support regex client attribute to validate redirect uris (#1005)
Browse files Browse the repository at this point in the history
* feat: support regex client property to validate redirect uris
  • Loading branch information
Milton-Ch authored Mar 11, 2022
1 parent a343215 commit a78ee1a
Show file tree
Hide file tree
Showing 20 changed files with 341 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -435,5 +435,6 @@
},
"deviceAuthzRequestExpiresIn": 1800,
"deviceAuthzTokenPollInterval": 5,
"deviceAuthzResponseTypeToProcessAuthz": "code"
"deviceAuthzResponseTypeToProcessAuthz": "code",
"redirectUrisRegexEnabled": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -359,5 +359,6 @@
"deviceAuthzTokenPollInterval": 5,
"deviceAuthzResponseTypeToProcessAuthz": "code",
"staticKid": "%(staticKid)s",
"forceOfflineAccessScopeToEnableRefreshToken" : false
"forceOfflineAccessScopeToEnableRefreshToken" : false,
"redirectUrisRegexEnabled": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public class RegisterRequest extends BaseRequest {
private AsymmetricSignatureAlgorithm backchannelAuthenticationRequestSigningAlg;
private Boolean backchannelUserCodeParameter;
private List<String> additionalAudience;
private String redirectUrisRegex;

/**
* String containing a space-separated list of scope values. (correct name is 'scope' not 'scopes', see (rfc7591).)
Expand Down Expand Up @@ -1396,6 +1397,10 @@ public Map<String, String> getParameters() {
parameters.put(DEFAULT_PROMPT_LOGIN.getName(), defaultPromptLogin.toString());
}

if (redirectUrisRegex != null) {
parameters.put(REDIRECT_URIS_REGEX.toString(), redirectUrisRegex.toString()) ;
}

// Custom params
if (customAttributes != null && !customAttributes.isEmpty()) {
for (Map.Entry<String, String> entry : customAttributes.entrySet()) {
Expand Down Expand Up @@ -1481,6 +1486,7 @@ public static RegisterRequest fromJson(JSONObject requestObject) throws JSONExce
result.setBackchannelClientNotificationEndpoint(requestObject.optString(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString()));
result.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.fromString(requestObject.optString(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())));
result.setBackchannelUserCodeParameter(booleanOrNull(requestObject, BACKCHANNEL_USER_CODE_PARAMETER.toString()));
result.setRedirectUrisRegex(requestObject.optString(REDIRECT_URIS_REGEX.toString()));
result.setDefaultPromptLogin(requestObject.optBoolean(DEFAULT_PROMPT_LOGIN.getName()));

return result;
Expand Down Expand Up @@ -1693,9 +1699,15 @@ public JSONObject getJSONParameters() throws JSONException {
if (backchannelUserCodeParameter != null) {
parameters.put(BACKCHANNEL_USER_CODE_PARAMETER.toString(), backchannelUserCodeParameter);
}

if (redirectUrisRegex != null) {
parameters.put(REDIRECT_URIS_REGEX.toString(), redirectUrisRegex) ;
}

if (defaultPromptLogin != null) {
parameters.put(DEFAULT_PROMPT_LOGIN.getName(), defaultPromptLogin);
}

// Custom params
if (customAttributes != null && !customAttributes.isEmpty()) {
for (Map.Entry<String, String> entry : customAttributes.entrySet()) {
Expand Down Expand Up @@ -1754,4 +1766,12 @@ public void setJwtRequestAsString(String jwtRequestAsString) {
public boolean hasJwtRequestAsString() {
return StringUtils.isNotBlank(jwtRequestAsString);
}
}

public String getRedirectUrisRegex() {
return redirectUrisRegex;
}

public void setRedirectUrisRegex(String redirectUrisRegex) {
this.redirectUrisRegex = redirectUrisRegex;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.jans.as.client.ws.rs;

import io.jans.as.client.*;
import io.jans.as.model.common.ResponseType;
import io.jans.as.model.register.ApplicationType;
import io.jans.as.model.util.StringUtils;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import static io.jans.as.client.client.Asserter.*;
import static io.jans.as.model.register.RegisterRequestParam.*;

/**
* Integration tests to validate redirect uris regex behavior
*
*/
public class AuthorizationRedirectUrisRegexTest extends BaseTest {

@Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri", "redirectUrisRegex", "redirectUri"})
@Test
public void requestClientValidateUsingRedirectUrisRegex( final String userId, final String userSecret,
final String redirectUris, final String sectorIdentifierUri,
final String redirectUrisRegex, final String redirectUri) {
showTitle("requestClientValidateUsingRedirectUrisRegex");

List<ResponseType> responseTypes = Arrays.asList(ResponseType.CODE);

// 1. Register client
RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "jans test app",
StringUtils.spaceSeparatedToList(redirectUris));
registerRequest.setResponseTypes(responseTypes);
registerRequest.setSectorIdentifierUri(sectorIdentifierUri);
registerRequest.setRedirectUrisRegex(redirectUrisRegex);
registerRequest.setRedirectUris(getRedirectUris());

RegisterClient registerClient = new RegisterClient(registrationEndpoint);
registerClient.setRequest(registerRequest);
RegisterResponse registerResponse = registerClient.exec();

showClient(registerClient);
assertRegisterResponseOk(registerResponse, 201, true);

String clientId = registerResponse.getClientId();
String registrationAccessToken = registerResponse.getRegistrationAccessToken();
String registrationClientUri = registerResponse.getRegistrationClientUri();

// 2. Client read
RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken);

RegisterClient readClient = new RegisterClient(registrationClientUri);
readClient.setRequest(readClientRequest);
RegisterResponse readClientResponse = readClient.exec();

showClient(readClient);
assertRegisterResponseOk(readClientResponse, 200, false);

assertRegisterResponseClaimsNotNull(readClientResponse, RESPONSE_TYPES, REDIRECT_URIS. APPLICATION_TYPE, CLIENT_NAME, ID_TOKEN_SIGNED_RESPONSE_ALG, SCOPE);

// 3. Request authorization and receive the authorization code.
List<String> scopes = Arrays.asList("openid", "profile", "address", "email");
String state = UUID.randomUUID().toString();

AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null);
authorizationRequest.setState(state);

AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess(
authorizationEndpoint, authorizationRequest, userId, userSecret);

assertAuthorizationResponse(authorizationResponse, true);

}

private List<String> getRedirectUris() {
return Arrays.asList("https://www.jans.org",
"http://localhost:80/jans-auth-rp/home.htm",
"https://localhost:8443/jans-auth-rp/home.htm",
"https://client.example.org/callback",
"https://client.example.org/callback2",
"https://client.other_company.example.net/callback",
"https://client.example.com/cb",
"https://client.example.com/cb1",
"https://client.example.com/cb2") ;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ clientSecret=${auth.client.secret}
audience=https://${test.server.name}
redirectUri=https://${test.server.name}/jans-auth-rp/home.htm
redirectUris=https://${test.server.name}/jans-auth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2
redirectUrisRegex=/^([a-z0-9+.-]+):(?://(?:((?:[a-z0-9-._~!$&'()*+,;=:]|%[0-9A-F]{2})*)@)?((?:[a-z0-9-._~!$&'()*+,;=]|%[0-9A-F]{2})*)(?::(\\d*))?(/(?:[a-z0-9-._~!$&'()*+,;=:@/]|%[0-9A-F]{2})*)?|(/?(?:[a-z0-9-._~!$&'()*+,;=:@]|%[0-9A-F]{2})+(?:[a-z0-9-._~!$&'()*+,;=:@/]|%[0-9A-F]{2})*)?)(?:\\?((?:[a-z0-9-._~!$&'()*+,;=:/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&'()*+,;=:/?@]|%[0-9A-F]{2})*))?$/i
#redirectUris=https://${test.server.name}/jans-auth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2 https://openid.implicit.client.test/login-callback.html
logoutUri=https://${test.server.name}/jans-auth-rp/home.htm
postLogoutRedirectUri=https://client.example.com/pl
Expand Down
1 change: 1 addition & 0 deletions jans-auth-server/client/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
<test name="Authorize test (HTTP)" enabled="true">
<classes>
<class name="io.jans.as.client.ws.rs.AuthorizeRestWebServiceHttpTest"/>
<class name="io.jans.as.client.ws.rs.AuthorizationRedirectUrisRegexTest"/>
</classes>
</test>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ public class AppConfiguration implements Configuration {
private int expirationNotificatorMapSizeLimit = 100000;
private int expirationNotificatorIntervalInSeconds = 600;

//feature flags
private Boolean redirectUrisRegexEnabled = false;

private Boolean authenticationFiltersEnabled;
private Boolean clientAuthenticationFiltersEnabled;
private Boolean clientRegDefaultToCodeFlowWithRefresh;
Expand Down Expand Up @@ -2459,4 +2462,12 @@ public int getDpopJtiCacheTime() {
public void setDpopJtiCacheTime(int dpopJtiCacheTime) {
this.dpopJtiCacheTime = dpopJtiCacheTime;
}

public Boolean getRedirectUrisRegexEnabled() {
return redirectUrisRegexEnabled != null && redirectUrisRegexEnabled;
}

public void setRedirectUrisRegexEnabled(Boolean redirectUrisRegexEnabled) {
this.redirectUrisRegexEnabled = redirectUrisRegexEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ public enum RegisterErrorResponseType implements IErrorType {
*/
ACCESS_DENIED("access_denied"),

INVALID_PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE("invalid_public_subject_identifier_attribute");
INVALID_PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE("invalid_public_subject_identifier_attribute"),

INVALID_REDIRECT_URIS_REGEX("invalid_redirect_uris_regex");

private final String paramName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,11 @@ public enum RegisterRequestParam {

BACKCHANNEL_USER_CODE_PARAMETER("backchannel_user_code_parameter"),

DEFAULT_PROMPT_LOGIN("default_prompt_login"),
PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE("public_subject_identifier_attribute"),

PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE("public_subject_identifier_attribute");
REDIRECT_URIS_REGEX("redirect_uris_regex"),

DEFAULT_PROMPT_LOGIN("default_prompt_login");

/**
* Parameter name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public class ClientAttributes implements Serializable {
@JsonProperty("jansSubAttr")
private String publicSubjectIdentifierAttribute;

@JsonProperty("redirectUrisRegex")
private String redirectUrisRegex ;

@JsonProperty("jansDefaultPromptLogin")
private Boolean defaultPromptLogin = false;

Expand Down Expand Up @@ -296,6 +299,14 @@ public void setPublicSubjectIdentifierAttribute(String publicSubjectIdentifierAt
this.publicSubjectIdentifierAttribute = publicSubjectIdentifierAttribute;
}

public String getRedirectUrisRegex() {
return redirectUrisRegex;
}

public void setRedirectUrisRegex(String redirectUrisRegex) {
this.redirectUrisRegex = redirectUrisRegex;
}

public Boolean getDefaultPromptLogin() {
if (defaultPromptLogin == null) {
defaultPromptLogin = false;
Expand Down Expand Up @@ -329,6 +340,7 @@ public String toString() {
", authorizationEncryptedResponseAlg=" + authorizationEncryptedResponseAlg +
", authorizationEncryptedResponseEnc=" + authorizationEncryptedResponseEnc +
", publicSubjectIdentifierAttribute=" + publicSubjectIdentifierAttribute +
", redirectUrisRegex=" + redirectUrisRegex +
", defaultPromptLogin=" + defaultPromptLogin +
'}';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ private Response registerClientImpl(String requestParams, HttpServletRequest htt

builder.entity(jsonObjectToString(jsonObject));

log.info("Client registered: clientId = {}, applicationType = {}, clientName = {}, redirectUris = {}, sectorIdentifierUri = {}",
client.getClientId(), client.getApplicationType(), client.getClientName(), client.getRedirectUris(), client.getSectorIdentifierUri());
log.info("Client registered: clientId = {}, applicationType = {}, clientName = {}, redirectUris = {}, sectorIdentifierUri = {}, redirectUrisRegex = {}" ,
client.getClientId(), client.getApplicationType(), client.getClientName(), client.getRedirectUris(), client.getSectorIdentifierUri(), client.getAttributes().getRedirectUrisRegex());

oAuth2AuditLog.setClientId(client.getClientId());
oAuth2AuditLog.setScope(clientScopesToString(client));
Expand Down Expand Up @@ -364,6 +364,16 @@ private void validateSubjectIdentifierAttribute(RegisterRequest registerRequest)
);
}
}

if (StringUtils.isNotBlank(registerRequest.getRedirectUrisRegex())) {
if (Boolean.FALSE.equals(appConfiguration.getRedirectUrisRegexEnabled())) {
throw errorResponseFactory.createBadRequestException(
RegisterErrorResponseType.INVALID_REDIRECT_URIS_REGEX,
"The redirect URI's Regex is disabled."
);
}
}

}

private void setClientName(RegisterRequest r, Client client) {
Expand Down Expand Up @@ -676,6 +686,9 @@ private void updateClientFromRequestObject(Client client, RegisterRequest reques
if (requestObject.getTlsClientAuthSubjectDn() != null) {
client.getAttributes().setTlsClientAuthSubjectDn(requestObject.getTlsClientAuthSubjectDn());
}
if (requestObject.getRedirectUrisRegex() != null) {
client.getAttributes().setRedirectUrisRegex(requestObject.getRedirectUrisRegex());
}
if (requestObject.getAllowSpontaneousScopes() != null) {
client.getAttributes().setAllowSpontaneousScopes(requestObject.getAllowSpontaneousScopes());
}
Expand Down Expand Up @@ -1136,6 +1149,7 @@ private JSONObject getJSONObject(Client client) throws JSONException, StringEncr
Util.addToJSONObjectIfNotNull(responseJsonObject, FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString(), client.getFrontChannelLogoutSessionRequired());
Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_LOGOUT_URI.toString(), client.getAttributes().getBackchannelLogoutUri());
Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_LOGOUT_SESSION_REQUIRED.toString(), client.getAttributes().getBackchannelLogoutSessionRequired());
Util.addToJSONObjectIfNotNull(responseJsonObject, REDIRECT_URIS_REGEX.toString(), client.getAttributes().getRedirectUrisRegex());

// Custom Params
String[] scopeNames = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,33 @@ public String validateRedirectionUri(@NotNull Client client, String redirectionU
redirectUris = getSectorRedirectUris(sectorIdentifierUri).toArray(new String[0]);
}

if (StringUtils.isNotBlank(redirectionUri) && redirectUris != null) {
log.debug("Validating redirection URI: clientIdentifier = {}, redirectionUri = {}, found = {}",
client.getClientId(), redirectionUri, redirectUris.length);
if (StringUtils.isBlank(sectorIdentifierUri) && redirectUris != null && redirectUris.length == 1) {
return redirectUris[0];
}

if (isUriEqual(redirectionUri, redirectUris)) {
return redirectionUri;
if (StringUtils.isNotBlank(redirectionUri)) {
if (redirectUris != null) {
log.debug("Validating redirection URI: clientIdentifier = {}, redirectionUri = {}, found = {}",
client.getClientId(), redirectionUri, redirectUris.length);
if (isUriEqual(redirectionUri, redirectUris)) {
return redirectionUri;
} else {
log.debug("RedirectionUri didn't match with any of the client redirect uris, clientId = {}, redirectionUri = {}", client.getClientId(), redirectionUri);
}
}
} else {
// Accept Request Without redirect_uri when One Registered
if (redirectUris != null && redirectUris.length == 1) {
return redirectUris[0];

if (appConfiguration.getRedirectUrisRegexEnabled()) {
if (redirectionUri.matches(client.getAttributes().getRedirectUrisRegex())) {
return redirectionUri;
} else {
log.debug("RedirectionUri didn't match with client regular expression, clientId = {}, redirectionUri = {}", client.getClientId(), redirectionUri);
}
}
} else {
log.warn("RedirectionUri is blank, clientId = {}", client.getClientId());
}
} catch (Exception e) {
log.error("Problems validating redirection uri, clientId = {}, redirectionUri = {}", client.getClientId(), redirectionUri);
return null;
}
return null;
Expand Down
Loading

0 comments on commit a78ee1a

Please sign in to comment.