Skip to content

Commit 1b57be7

Browse files
Okta 972482 idx language issue (#521)
* adding support for locale preferred language * Add accept language in proceed requests --------- Co-authored-by: Prachi Pandey <[email protected]>
1 parent 84125e0 commit 1b57be7

File tree

7 files changed

+148
-16
lines changed

7 files changed

+148
-16
lines changed

api/src/main/java/com/okta/idx/sdk/api/client/BaseIDXClient.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@
6363
import java.io.IOException;
6464
import java.nio.charset.StandardCharsets;
6565
import java.security.NoSuchAlgorithmException;
66+
import java.util.Collections;
6667
import java.util.UUID;
6768
import java.util.stream.Collectors;
6869

70+
import static com.okta.idx.sdk.api.client.ProceedContext.ACCEPT_LANGUAGE;
6971
import static com.okta.idx.sdk.api.util.ClientUtil.normalizedIssuerUri;
7072

7173
final class BaseIDXClient implements IDXClient {
@@ -280,17 +282,28 @@ public IDXResponse enroll(EnrollRequest enrollRequest, String href) throws Proce
280282

281283
@Override
282284
public IDXResponse challenge(ChallengeRequest challengeRequest, String href) throws ProcessingException {
285+
return challenge(challengeRequest, href, null);
286+
}
283287

288+
@Override
289+
public IDXResponse challenge(ChallengeRequest challengeRequest, String href, String acceptLanguage) throws ProcessingException {
284290
IDXResponse idxResponse;
285291

286292
try {
293+
HttpHeaders headers = getHttpHeaders(false);
294+
295+
if (acceptLanguage != null) {
296+
headers.put(ACCEPT_LANGUAGE, Collections.singletonList(acceptLanguage));
297+
}
298+
287299
Request request = new DefaultRequest(
288-
HttpMethod.POST,
289-
href,
290-
null,
291-
getHttpHeaders(false),
292-
new ByteArrayInputStream(objectMapper.writeValueAsBytes(challengeRequest)),
293-
-1L);
300+
HttpMethod.POST,
301+
href,
302+
null,
303+
headers,
304+
new ByteArrayInputStream(objectMapper.writeValueAsBytes(challengeRequest)),
305+
-1L
306+
);
294307

295308
Response response = requestExecutor.executeRequest(request);
296309

@@ -299,7 +312,6 @@ public IDXResponse challenge(ChallengeRequest challengeRequest, String href) thr
299312
}
300313

301314
JsonNode responseJsonNode = objectMapper.readTree(response.getBody());
302-
303315
idxResponse = objectMapper.convertValue(responseJsonNode, IDXResponse.class);
304316

305317
} catch (IOException | HttpException e) {

api/src/main/java/com/okta/idx/sdk/api/client/IDXAuthenticationWrapper.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public AuthenticationResponse authenticate(AuthenticationOptions authenticationO
161161
return identifyResponse;
162162
}
163163

164-
AuthenticationTransaction passwordTransaction = selectPasswordOrEmailAuthenticatorIfNeeded(identifyTransaction);
164+
AuthenticationTransaction passwordTransaction = selectPasswordOrEmailAuthenticatorIfNeeded(identifyTransaction, proceedContext.getAcceptLanguage());
165165
if (Strings.isEmpty(authenticationOptions.getPassword())) {
166166
return passwordTransaction.asAuthenticationResponse(AuthenticationStatus.AWAITING_AUTHENTICATOR_VERIFICATION);
167167
}
@@ -178,7 +178,7 @@ public AuthenticationResponse authenticate(AuthenticationOptions authenticationO
178178
.build();
179179

180180
return passwordTransaction.getRemediationOption(RemediationType.CHALLENGE_AUTHENTICATOR)
181-
.proceed(client, passwordAuthenticatorAnswerChallengeRequest);
181+
.proceed(client, passwordAuthenticatorAnswerChallengeRequest, proceedContext.getAcceptLanguage());
182182
});
183183
return answerTransaction.asAuthenticationResponse();
184184
} catch (ProcessingException e) {
@@ -218,7 +218,7 @@ public AuthenticationResponse recoverPassword(String username, ProceedContext pr
218218

219219
// identify user
220220
return recoverTransaction.proceed(() ->
221-
remediationOption.proceed(client, identifyRequest)
221+
remediationOption.proceed(client, identifyRequest, proceedContext.getAcceptLanguage())
222222
).asAuthenticationResponse(AuthenticationStatus.AWAITING_AUTHENTICATOR_SELECTION);
223223
} else {
224224
// identify user
@@ -237,7 +237,7 @@ public AuthenticationResponse recoverPassword(String username, ProceedContext pr
237237

238238
// Check if instead of password, user is being prompted for list of authenticators to select
239239
if (identifyResponse.getCurrentAuthenticatorEnrollment() == null) {
240-
identifyTransaction = selectPasswordOrEmailAuthenticatorIfNeeded(identifyTransaction);
240+
identifyTransaction = selectPasswordOrEmailAuthenticatorIfNeeded(identifyTransaction, proceedContext.getAcceptLanguage());
241241
}
242242

243243
Recover recover = identifyTransaction.getResponse()
@@ -349,7 +349,7 @@ public AuthenticationResponse selectFactor(ProceedContext proceedContext,
349349
.withStateHandle(proceedContext.getStateHandle())
350350
.withAuthenticator(authenticator)
351351
.build();
352-
return client.challenge(request, proceedContext.getHref());
352+
return client.challenge(request, proceedContext.getHref(), proceedContext.getAcceptLanguage());
353353
}).asAuthenticationResponse();
354354
} catch (ProcessingException e) {
355355
return handleProcessingException(e);
@@ -734,7 +734,7 @@ public AuthenticationResponse fetchSignUpFormValues(ProceedContext proceedContex
734734
// If app sign-on policy is set to "any 1 factor", the next remediation after identify is
735735
// select-authenticator-authenticate
736736
// Check if that's the case, and proceed to select password authenticator
737-
private AuthenticationTransaction selectPasswordOrEmailAuthenticatorIfNeeded(AuthenticationTransaction authenticationTransaction)
737+
private AuthenticationTransaction selectPasswordOrEmailAuthenticatorIfNeeded(AuthenticationTransaction authenticationTransaction, String acceptLanguage)
738738
throws ProcessingException {
739739
// If remediation contains challenge-authenticator for passcode, we don't need to check SELECT_AUTHENTICATOR_AUTHENTICATE
740740
Optional<RemediationOption> challengeRemediationOptionOptional =
@@ -769,7 +769,7 @@ else if (authenticatorOptions.get("email") != null) {
769769
.build();
770770

771771
return authenticationTransaction.proceed(() ->
772-
remediationOptionOptional.get().proceed(client, selectAuthenticatorRequest)
772+
remediationOptionOptional.get().proceed(client, selectAuthenticatorRequest, acceptLanguage)
773773
);
774774
}
775775

api/src/main/java/com/okta/idx/sdk/api/client/IDXClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public interface IDXClient {
4848

4949
IDXResponse challenge(ChallengeRequest challengeRequest, String href) throws ProcessingException;
5050

51+
IDXResponse challenge(ChallengeRequest challengeRequest, String href, String acceptLanguage) throws ProcessingException;
52+
5153
IDXResponse answerChallenge(AnswerChallengeRequest answerChallengeRequest, String href) throws ProcessingException;
5254

5355
IDXResponse cancel(String stateHandle) throws ProcessingException;

api/src/main/java/com/okta/idx/sdk/api/client/ProceedContext.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
import java.time.Duration;
2525
import java.time.temporal.ChronoUnit;
26+
import java.util.LinkedHashMap;
27+
import java.util.Map;
2628

2729
/**
2830
* An opaque to the developer object that's expected to be given back on the next request.
@@ -39,6 +41,12 @@ public final class ProceedContext {
3941
private final PollInfo pollInfo;
4042
private final Duration refresh;
4143
private final IDXResponse idxResponse;
44+
public static final String ACCEPT_LANGUAGE = "Accept-Language";
45+
46+
/**
47+
* store key value pairs of header name -> header value
48+
*/
49+
private final Map<String, String> headers = new LinkedHashMap<>();
4250

4351
ProceedContext(IDXClientContext clientContext, String stateHandle, String href, String skipHref, boolean isIdentifyInOneStep,
4452
String selectProfileEnrollHref, String resendHref, PollInfo pollInfo, Duration refresh, IDXResponse idxResponse) {
@@ -104,6 +112,7 @@ public Duration getRefresh() {
104112
/**
105113
* Identifier first flow is one where just the identifier (email) is sufficient to start
106114
* the flow (i.e. password is not required at the start of flow).
115+
*
107116
* @return true if identifier first flow, false otherwise
108117
*/
109118
public boolean isIdentifierFirstFlow() {
@@ -113,4 +122,12 @@ public boolean isIdentifierFirstFlow() {
113122
IDXResponse getIdxResponse() {
114123
return idxResponse;
115124
}
125+
126+
public String getAcceptLanguage() {
127+
return headers.get(ACCEPT_LANGUAGE);
128+
}
129+
130+
public void setAcceptLanguage(String acceptLanguage) {
131+
headers.put(ACCEPT_LANGUAGE, acceptLanguage);
132+
}
116133
}

api/src/main/java/com/okta/idx/sdk/api/model/RemediationOption.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,26 @@ public class RemediationOption implements Serializable {
8686
* @throws ProcessingException when the proceed operation encountered an execution/processing error.
8787
*/
8888
public IDXResponse proceed(IDXClient client, Object request) throws IllegalStateException, IllegalArgumentException, ProcessingException {
89+
return proceed(client, request, null);
90+
}
91+
92+
/**
93+
* Allow you to continue the remediation with this option.
94+
*
95+
* @param client the {@link IDXClient} instance
96+
* @param request the request to Okta Identity Engine
97+
* @param acceptLanguage the {@code Accept-Language} header value to be sent to the Okta Identity Engine. This is used for localization.
98+
* @return IDXResponse the response from Okta Identity Engine
99+
*
100+
* @throws IllegalArgumentException MUST throw this exception when provided data does not contain all required data for the proceed call.
101+
* @throws IllegalStateException MUST throw this exception when proceed is called with an invalid/unsupported request type.
102+
* @throws ProcessingException when the proceed operation encountered an execution/processing error.
103+
*/
104+
public IDXResponse proceed(IDXClient client, Object request, String acceptLanguage) throws IllegalStateException, IllegalArgumentException, ProcessingException {
89105
Assert.notNull(request, "request cannot be null");
90106

91107
if (request instanceof IdentifyRequest) return client.identify((IdentifyRequest) request, href);
92-
else if (request instanceof ChallengeRequest) return client.challenge((ChallengeRequest) request, href);
108+
else if (request instanceof ChallengeRequest) return client.challenge((ChallengeRequest) request, href, acceptLanguage);
93109
else if (request instanceof AnswerChallengeRequest)
94110
return client.answerChallenge((AnswerChallengeRequest) request, href);
95111
else if (request instanceof EnrollRequest) return client.enroll((EnrollRequest) request, href);

integration-tests/src/test/groovy/com/okta/idx/sdk/tests/it/EndToEndIT.groovy

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,88 @@ class EndToEndIT {
488488
wireMockServer.resetAll()
489489
}
490490

491+
@Test
492+
void testChallengeWithAcceptLanguageHeader() {
493+
494+
// interact
495+
wireMockServer.stubFor(post(urlPathEqualTo("/oauth2/v1/interact"))
496+
.withHeader("Content-Type", containing(MediaType.APPLICATION_FORM_URLENCODED_VALUE))
497+
.willReturn(aResponse()
498+
.withStatus(HttpStatus.SC_OK)
499+
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
500+
.withBodyFile("interact-response.json")))
501+
502+
IDXClientContext idxClientContext = idxClient.interact()
503+
wireMockServer.resetAll()
504+
505+
// introspect
506+
wireMockServer.stubFor(post(urlPathEqualTo("/idp/idx/introspect"))
507+
.withHeader("Content-Type", containing("application/ion+json;okta-version=1.0.0"))
508+
.willReturn(aResponse()
509+
.withStatus(HttpStatus.SC_OK)
510+
.withHeader("Content-Type", "application/ion+json;okta-version=1.0.0")
511+
.withBodyFile("introspect-response.json")))
512+
513+
IDXResponse idxResponse = idxClient.introspect(idxClientContext)
514+
wireMockServer.resetAll()
515+
516+
// identify
517+
wireMockServer.stubFor(post(urlPathEqualTo("/idp/idx/identify"))
518+
.withHeader("Content-Type", containing("application/ion+json;okta-version=1.0.0"))
519+
.willReturn(aResponse()
520+
.withStatus(HttpStatus.SC_OK)
521+
.withHeader("Content-Type", "application/ion+json;okta-version=1.0.0")
522+
.withBodyFile("identify-response.json")))
523+
524+
IdentifyRequest identifyRequest = IdentifyRequestBuilder.builder()
525+
.withIdentifier("[email protected]")
526+
.withRememberMe(false)
527+
.withStateHandle("stateHandle")
528+
.build()
529+
idxResponse = idxResponse.remediation().remediationOptions().first().proceed(idxClient, identifyRequest)
530+
wireMockServer.verify(postRequestedFor(urlEqualTo("/idp/idx/identify"))
531+
.withHeader("Content-Type", equalTo("application/ion+json;okta-version=1.0.0"))
532+
.withoutHeader("Accept-Language"))
533+
wireMockServer.resetAll()
534+
535+
// get remediation options to go to the next step
536+
RemediationOption[] remediationOptions = idxResponse.remediation().remediationOptions()
537+
Optional<RemediationOption> remediationOptionsOptional = Arrays.stream(remediationOptions)
538+
.filter({ x -> ("select-authenticator-authenticate" == x.getName()) })
539+
.findFirst()
540+
RemediationOption remediationOption = remediationOptionsOptional.get()
541+
542+
// get authenticator options
543+
Map<String, String> authenticatorOptionsMap = remediationOption.getAuthenticatorOptions()
544+
545+
// select password authenticator challenge
546+
wireMockServer.stubFor(post(urlPathEqualTo("/idp/idx/challenge"))
547+
.withHeader("Content-Type", containing("application/ion+json;okta-version=1.0.0"))
548+
.willReturn(aResponse()
549+
.withStatus(HttpStatus.SC_OK)
550+
.withHeader("Content-Type", "application/ion+json;okta-version=1.0.0")
551+
.withBodyFile("password-authenticator-challenge-response.json")))
552+
553+
Authenticator passwordAuthenticator = new Authenticator()
554+
passwordAuthenticator.setId(authenticatorOptionsMap.get("password"))
555+
passwordAuthenticator.setMethodType("password")
556+
557+
ChallengeRequest passwordAuthenticatorChallengeRequest = ChallengeRequestBuilder.builder()
558+
.withStateHandle("stateHandle")
559+
.withAuthenticator(passwordAuthenticator)
560+
.build()
561+
562+
String acceptLanguage = "fr-FR"
563+
idxResponse = remediationOption.proceed(idxClient, passwordAuthenticatorChallengeRequest, acceptLanguage)
564+
565+
assertThat(idxResponse, notNullValue())
566+
567+
wireMockServer.verify(postRequestedFor(urlEqualTo("/idp/idx/challenge"))
568+
.withHeader("Content-Type", equalTo("application/ion+json;okta-version=1.0.0"))
569+
.withHeader("Accept-Language", equalTo(acceptLanguage)))
570+
wireMockServer.resetAll()
571+
}
572+
491573
@AfterClass
492574
void cleanUp() {
493575
wireMockServer.shutdown()

samples/embedded-auth-with-sdk/src/main/java/com/okta/spring/example/controllers/LoginController.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
import jakarta.servlet.http.HttpSession;
5050
import java.util.List;
51+
import java.util.Locale;
5152
import java.util.Optional;
5253
import java.util.stream.Collectors;
5354

@@ -202,7 +203,7 @@ public ModelAndView selectAuthenticator(final @RequestParam("authenticator-type"
202203

203204
if ("okta_verify".equals(authenticatorType)) {
204205
ModelAndView modelAndView;
205-
206+
String systemLocale = Locale.getDefault().toLanguageTag();
206207
Optional<Authenticator> authenticatorOptional = authenticators.stream()
207208
.filter(auth -> auth.getType().equals(authenticatorType)).findFirst();
208209
Assert.isTrue(authenticatorOptional.isPresent(), "Authenticator not found");
@@ -212,6 +213,8 @@ public ModelAndView selectAuthenticator(final @RequestParam("authenticator-type"
212213
.filter(x -> "QRCODE".equals(x.getLabel())).findFirst();
213214
Assert.isTrue(factorOptional.isPresent(), "Authenticator not found");
214215

216+
217+
proceedContext.setAcceptLanguage(systemLocale);
215218
authenticationResponse = idxAuthenticationWrapper.selectFactor(proceedContext, factorOptional.get());
216219
Util.setProceedContextForPoll(session, authenticationResponse.getProceedContext());
217220

0 commit comments

Comments
 (0)