Skip to content

Commit 9fa9386

Browse files
Merge pull request #9 from ZiyamSanthosh/main-enable-api-auth
Modify apple authenticator to enable federation flow in API based Authentication and Nonce validation
2 parents a024949 + d235a14 commit 9fa9386

File tree

4 files changed

+134
-23
lines changed

4 files changed

+134
-23
lines changed

components/org.wso2.carbon.identity.application.authenticator.apple/src/main/java/org/wso2/carbon/identity/application/authenticator/apple/AppleAuthenticator.java

+81-21
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,25 @@
7878
import java.util.Map;
7979
import java.util.Optional;
8080
import java.util.Set;
81+
import java.util.UUID;
8182
import java.util.regex.Matcher;
8283
import java.util.regex.Pattern;
8384
import java.util.stream.Collectors;
8485

8586
import javax.servlet.http.HttpServletRequest;
8687
import javax.servlet.http.HttpServletResponse;
8788

89+
import static org.wso2.carbon.identity.application.authenticator.apple.AppleAuthenticatorConstants.AUTHENTICATOR_APPLE;
90+
import static org.wso2.carbon.identity.application.authenticator.apple.AppleAuthenticatorConstants.COMMA_SEPARATOR;
8891
import static org.wso2.carbon.identity.application.authenticator.apple.AppleAuthenticatorConstants.LogConstants.ActionIDs.PROCESS_AUTHENTICATION_RESPONSE;
8992
import static org.wso2.carbon.identity.application.authenticator.apple.AppleAuthenticatorConstants.LogConstants.ActionIDs.VALIDATE_OUTBOUND_AUTH_REQUEST;
93+
import static org.wso2.carbon.identity.application.authenticator.apple.AppleAuthenticatorConstants.PLUS_SEPARATOR;
94+
import static org.wso2.carbon.identity.application.authenticator.apple.AppleAuthenticatorConstants.SPACE_SEPARATOR;
95+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.Claim.NONCE;
96+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.OIDC_FEDERATION_NONCE;
97+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.REDIRECT_URL_SUFFIX;
98+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.SCOPE_PARAM_SUFFIX;
99+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.STATE_PARAM_SUFFIX;
90100
import static org.wso2.carbon.identity.base.IdentityConstants.FEDERATED_IDP_SESSION_ID;
91101

92102
/**
@@ -144,9 +154,13 @@ protected void initiateAuthenticationRequest(HttpServletRequest request, HttpSer
144154

145155
String clientId = authenticatorProperties.get(OIDCAuthenticatorConstants.CLIENT_ID);
146156
String authorizationEP = getAuthorizationServerEndpoint(authenticatorProperties);
147-
String callbackUrl = getCallbackUrl(authenticatorProperties);
148-
String state = getStateParameter(context, authenticatorProperties);
157+
String callbackUrl = getCallbackUrl(authenticatorProperties, context);
158+
String state = getStateParameter(request, context, authenticatorProperties);
159+
context.setProperty(getName() + STATE_PARAM_SUFFIX, state);
160+
String nonce = UUID.randomUUID().toString();
161+
context.setProperty(getName() + OIDC_FEDERATION_NONCE, nonce);
149162
String scopes = getScope(authenticatorProperties);
163+
context.setProperty(getName() + SCOPE_PARAM_SUFFIX, scopes);
150164
String queryString = getQueryString(authenticatorProperties);
151165

152166
// If scopes are present, Apple requires sending response_mode as form_post.
@@ -174,7 +188,8 @@ protected void initiateAuthenticationRequest(HttpServletRequest request, HttpSer
174188
OAuthClientRequest.AuthenticationRequestBuilder requestBuilder =
175189
OAuthClientRequest.authorizationLocation(authorizationEP).setClientId(clientId)
176190
.setResponseType(OIDCAuthenticatorConstants.OAUTH2_GRANT_TYPE_CODE)
177-
.setState(state);
191+
.setState(state)
192+
.setParameter(NONCE, nonce);
178193
if (LoggerUtils.isDiagnosticLogsEnabled() && diagnosticLogBuilder != null) {
179194
diagnosticLogBuilder.inputParam("scopes", scope);
180195
}
@@ -200,6 +215,7 @@ protected void initiateAuthenticationRequest(HttpServletRequest request, HttpSer
200215
loginPage = loginPage + queryString;
201216
}
202217
}
218+
context.setProperty(getName() + REDIRECT_URL_SUFFIX, loginPage);
203219
response.sendRedirect(response.encodeRedirectURL(loginPage.replace("\r\n", "")));
204220
if (LoggerUtils.isDiagnosticLogsEnabled() && diagnosticLogBuilder != null) {
205221
diagnosticLogBuilder.resultMessage("Redirected to the Apple Id login page.");
@@ -341,6 +357,21 @@ protected void processAuthenticationResponse(HttpServletRequest request, HttpSer
341357
}
342358
}
343359

360+
// Nonce validation.
361+
String nonceKey = getName() + OIDC_FEDERATION_NONCE;
362+
if (StringUtils.isNotBlank((String) context.getProperty(nonceKey))) {
363+
String nonce = (String) jwtAttributeMap.get(NONCE);
364+
if (nonce == null) {
365+
log.debug("OIDC provider does not support nonce claim in id_token.");
366+
}
367+
if (nonce != null && !nonce.equals(context.getProperty(nonceKey))) {
368+
setAuthenticatorMessageToContext(OIDCErrorConstants.ErrorMessages.NONCE_MISMATCH, context);
369+
370+
throw new AuthenticationFailedException(OIDCErrorConstants.ErrorMessages.NONCE_MISMATCH.getCode(),
371+
OIDCErrorConstants.ErrorMessages.NONCE_MISMATCH.getMessage());
372+
}
373+
}
374+
344375
String authenticatedUserId = getAuthenticatedUserId(context, oAuthResponse, jwtAttributeMap);
345376
String attributeSeparator = getMultiAttributeSeparator(context, authenticatedUserId);
346377

@@ -636,6 +667,44 @@ protected String getComponentId() {
636667
return AppleAuthenticatorConstants.LogConstants.OUTBOUND_AUTH_APPLE_SERVICE;
637668
}
638669

670+
/**
671+
* Get the i18n key defined to represent the authenticator name.
672+
*
673+
* @return the 118n key.
674+
*/
675+
@Override
676+
public String getI18nKey() {
677+
678+
return AUTHENTICATOR_APPLE;
679+
}
680+
681+
/**
682+
* This method is responsible for validating whether the authenticator is supported for API Based Authentication.
683+
*
684+
* @return true if the authenticator is supported for API Based Authentication.
685+
*/
686+
@Override
687+
public boolean isAPIBasedAuthenticationSupported() {
688+
689+
return true;
690+
}
691+
692+
/**
693+
* This method is responsible to return the scopes as space separated string.
694+
*
695+
* @param authenticatorProperties Authenticator properties.
696+
* @return Scopes string
697+
*/
698+
@Override
699+
protected String getScope(Map<String, String> authenticatorProperties) {
700+
701+
String scopes = authenticatorProperties.get(IdentityApplicationConstants.Authenticator.OIDC.SCOPES);
702+
if (StringUtils.isNotBlank(scopes)) {
703+
return scopes.replace(COMMA_SEPARATOR, SPACE_SEPARATOR).replace(PLUS_SEPARATOR, SPACE_SEPARATOR);
704+
}
705+
return scopes;
706+
}
707+
639708
/**
640709
* Evaluates if the client secret should be generated and if required, generate and store the secret.
641710
*
@@ -811,13 +880,20 @@ private void initTokenEndpoint() {
811880
/**
812881
* Get state parameter.
813882
*
883+
* @param request Authentication request.
814884
* @param context Authentication context.
815885
* @param authenticatorProperties Authenticator properties.
816886
* @return State.
817887
*/
818-
private String getStateParameter(AuthenticationContext context, Map<String, String> authenticatorProperties) {
888+
private String getStateParameter(HttpServletRequest request, AuthenticationContext context,
889+
Map<String, String> authenticatorProperties) {
819890

820-
String state = context.getContextIdentifier() + "," + OIDCAuthenticatorConstants.LOGIN_TYPE;
891+
String state;
892+
if (FrameworkUtils.isAPIBasedAuthenticationFlow(request)) {
893+
state = UUID.randomUUID() + "," + OIDCAuthenticatorConstants.LOGIN_TYPE;
894+
} else {
895+
state = context.getContextIdentifier() + "," + OIDCAuthenticatorConstants.LOGIN_TYPE;
896+
}
821897
return getState(state, authenticatorProperties);
822898
}
823899

@@ -1014,22 +1090,6 @@ private String getMultiAttributeSeparator(AuthenticationContext context, String
10141090
return attributeSeparator;
10151091
}
10161092

1017-
/**
1018-
* Get application details from the authentication context.
1019-
* @param context Authentication context.
1020-
* @return Map of application details.
1021-
*/
1022-
private Map<String, String> getApplicationDetails(AuthenticationContext context) {
1023-
1024-
Map<String, String> applicationDetailsMap = new HashMap<>();
1025-
FrameworkUtils.getApplicationResourceId(context).ifPresent(applicationId ->
1026-
applicationDetailsMap.put(LogConstants.InputKeys.APPLICATION_ID, applicationId));
1027-
FrameworkUtils.getApplicationName(context).ifPresent(applicationName ->
1028-
applicationDetailsMap.put(LogConstants.InputKeys.APPLICATION_NAME,
1029-
applicationName));
1030-
return applicationDetailsMap;
1031-
}
1032-
10331093
private static List<String> getUserAttributeClaimMappingList(AuthenticatedUser authenticatedUser) {
10341094

10351095
return authenticatedUser.getUserAttributes().keySet().stream()

components/org.wso2.carbon.identity.application.authenticator.apple/src/main/java/org/wso2/carbon/identity/application/authenticator/apple/AppleAuthenticatorConstants.java

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ private AppleAuthenticatorConstants() {
4949
public static final String APPLE_USER_INFO_KEY = "user";
5050
public static final String CLAIM_DIALECT_URI_PARAMETER = "ClaimDialectUri";
5151

52+
public static final String AUTHENTICATOR_APPLE = "authenticator.apple";
53+
54+
public static final String COMMA_SEPARATOR = ",";
55+
public static final String PLUS_SEPARATOR = "+";
56+
public static final String SPACE_SEPARATOR = " ";
57+
5258
/**
5359
* Constants related to log management.
5460
*/

components/org.wso2.carbon.identity.application.authenticator.apple/src/test/java/org/wso2/carbon/identity/application/authenticator/apple/AppleAuthenticatorTest.java

+45
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException;
3737
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
3838
import org.wso2.carbon.identity.application.authentication.framework.model.CommonAuthResponseWrapper;
39+
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants;
3940
import org.wso2.carbon.identity.application.authenticator.apple.internal.AppleAuthenticatorDataHolder;
4041
import org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants;
4142
import org.wso2.carbon.identity.application.authenticator.oidc.model.OIDCStateInfo;
@@ -77,6 +78,9 @@
7778
import static org.mockito.Mockito.verify;
7879
import static org.mockito.Mockito.when;
7980
import static org.mockito.MockitoAnnotations.openMocks;
81+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.REDIRECT_URL_SUFFIX;
82+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.SCOPE_PARAM_SUFFIX;
83+
import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.STATE_PARAM_SUFFIX;
8084

8185
/**
8286
* Unit tests for the Apple authenticator class.
@@ -87,6 +91,7 @@ public class AppleAuthenticatorTest {
8791
private static Map<String, String> testAuthenticatorProperties;
8892
private static AuthenticatorConfig authenticatorConfig;
8993
private static AuthenticationContext authenticationContext;
94+
private static boolean isAPIBased;
9095

9196
private AutoCloseable autoCloseable;
9297
@Mock
@@ -228,6 +233,28 @@ public void testInitiateAuthenticationRequest(String secretGenerateCondition)
228233
Assert.assertTrue(redirectUrl.contains("client_id=testClientId"));
229234
Assert.assertTrue(redirectUrl.contains("scope=name%20email"));
230235
Assert.assertTrue(redirectUrl.contains("response_mode=form_post"));
236+
237+
if (isAPIBased) {
238+
Assert.assertTrue(Boolean.parseBoolean(
239+
(String) authenticationContext.getProperty(FrameworkConstants.IS_API_BASED)));
240+
Assert.assertNotNull(authenticationContext.getProperty(appleAuthenticator.getName()
241+
+ STATE_PARAM_SUFFIX));
242+
Assert.assertNotNull(authenticationContext.getProperty(appleAuthenticator.getName()
243+
+ SCOPE_PARAM_SUFFIX));
244+
String redirectUrlPropertyKey = appleAuthenticator.getName() + REDIRECT_URL_SUFFIX;
245+
Assert.assertNotNull(authenticationContext.getProperty(redirectUrlPropertyKey));
246+
// For API based auth flow, the redirect URL should not be equal to commonauth endpoint.
247+
Assert.assertFalse(redirectUrl.contains("https://localhost:9443/commonauth"));
248+
}
249+
}
250+
251+
@Test(dataProvider = "initiateAuthenticationRequestDataProvider",
252+
dependsOnMethods = {"testInitiateAuthenticationRequest"})
253+
public void testInitiateAuthenticationRequestForAPIBased(String secretGenerateCondition)
254+
throws AuthenticationFailedException, IOException, IdentityProviderManagementException {
255+
256+
isAPIBased = true;
257+
testInitiateAuthenticationRequest(secretGenerateCondition);
231258
}
232259

233260
@DataProvider(name = "initiateAuthenticationRequestExceptionDataProvider")
@@ -522,6 +549,20 @@ public void testGetConfigurationProperties() {
522549
});
523550
}
524551

552+
@Test
553+
public void testGetI18nKey() {
554+
555+
String facebookI18nKey = appleAuthenticator.getI18nKey();
556+
Assert.assertEquals(facebookI18nKey, AppleAuthenticatorConstants.AUTHENTICATOR_APPLE);
557+
}
558+
559+
@Test
560+
public void testIsAPIBasedAuthenticationSupported() {
561+
562+
boolean isAPIBasedAuthenticationSupported = appleAuthenticator.isAPIBasedAuthenticationSupported();
563+
Assert.assertTrue(isAPIBasedAuthenticationSupported);
564+
}
565+
525566
private void initAuthenticatorProperties() {
526567

527568
testAuthenticatorProperties = new HashMap<>();
@@ -569,5 +610,9 @@ private void initAuthenticationContext() {
569610
authenticationContext.setTenantDomain(TEST_TENANT);
570611
authenticationContext.setExternalIdP(externalIdPConfig);
571612
authenticationContext.setContextIdentifier(CONTEXT_IDENTIFIER);
613+
614+
if (isAPIBased) {
615+
authenticationContext.setProperty(FrameworkConstants.IS_API_BASED, "true");
616+
}
572617
}
573618
}

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -326,11 +326,11 @@
326326
<carbon.kernel.package.import.version.range>[4.9.10, 5.0.0)</carbon.kernel.package.import.version.range>
327327
<carbon.user.api.imp.pkg.version.range>[1.0.1, 2.0.0)</carbon.user.api.imp.pkg.version.range>
328328
<carbon.base.imp.pkg.version.range>[1.0.0, 2.0.0)</carbon.base.imp.pkg.version.range>
329-
<carbon.identity.framework.version>5.25.260</carbon.identity.framework.version>
329+
<carbon.identity.framework.version>5.25.560</carbon.identity.framework.version>
330330
<identity.framework.package.import.version.range>[5.25.260, 8.0.0)</identity.framework.package.import.version.range>
331331

332332
<!-- other wso2 dependencies -->
333-
<identity.outbound.auth.oidc.version>5.11.18</identity.outbound.auth.oidc.version>
333+
<identity.outbound.auth.oidc.version>5.12.11</identity.outbound.auth.oidc.version>
334334
<carbon.identity.outbound.oidc.package.import.version.range>[5.11.18, 6.0.0)</carbon.identity.outbound.oidc.package.import.version.range>
335335
<carbon.identity.oauth.version>6.11.97</carbon.identity.oauth.version>
336336
<carbon.identity.oauth.package.import.version.range>[6.2.0, 8.0.0)</carbon.identity.oauth.package.import.version.range>

0 commit comments

Comments
 (0)