1+ package com.okta.directauth.http
2+
3+ import com.okta.authfoundation.GrantType
4+ import com.okta.authfoundation.api.http.ApiRequestMethod
5+ import com.okta.authfoundation.api.http.log.AuthFoundationLogger
6+ import com.okta.authfoundation.api.http.log.LogLevel
7+ import com.okta.directauth.model.DirectAuthenticationContext
8+ import com.okta.directauth.model.DirectAuthenticationError
9+ import com.okta.directauth.model.DirectAuthenticationIntent
10+ import com.okta.directauth.model.DirectAuthenticationState
11+ import com.okta.directauth.model.OobChannel
12+ import com.okta.directauth.notJsonMockEngine
13+ import com.okta.directauth.oobAuthenticateOauth2ErrorMockEngine
14+ import com.okta.directauth.oobAuthenticateResponseMockEngine
15+ import com.okta.directauth.serverErrorMockEngine
16+ import com.okta.directauth.unknownJsonTypeMockEngine
17+ import io.ktor.client.HttpClient
18+ import io.ktor.client.engine.mock.MockEngine
19+ import io.ktor.client.engine.mock.respond
20+ import io.ktor.http.HttpHeaders
21+ import io.ktor.http.HttpStatusCode
22+ import io.ktor.http.headersOf
23+ import kotlinx.coroutines.test.runTest
24+ import org.hamcrest.CoreMatchers.equalTo
25+ import org.hamcrest.CoreMatchers.instanceOf
26+ import org.hamcrest.CoreMatchers.nullValue
27+ import org.hamcrest.MatcherAssert.assertThat
28+ import org.junit.Before
29+ import org.junit.Test
30+
31+ class DirectAuthOobAuthenticateRequestTest {
32+ private lateinit var context: DirectAuthenticationContext
33+
34+ @Before
35+ fun setUp () {
36+ context = DirectAuthenticationContext (
37+ issuerUrl = " https://example.okta.com" ,
38+ clientId = " test_client_id" ,
39+ scope = listOf (" openid" , " email" , " profile" , " offline_access" ),
40+ authorizationServerId = " " ,
41+ clientSecret = " " ,
42+ grantTypes = listOf (GrantType .Password , GrantType .Otp ),
43+ acrValues = emptyList(),
44+ directAuthenticationIntent = DirectAuthenticationIntent .SIGN_IN ,
45+ apiExecutor = KtorHttpExecutor (),
46+ logger = object : AuthFoundationLogger {
47+ override fun write (message : String , tr : Throwable ? , logLevel : LogLevel ) {
48+ // No-op logger for tests
49+ }
50+ },
51+ clock = { 1654041600 }, // 2022-06-01
52+ additionalParameters = mapOf ()
53+ )
54+ }
55+
56+ @Test
57+ fun oobAuthenticateRequest_WithDefaultParameters () {
58+ val request = DirectAuthOobAuthenticateRequest (
59+ context = context,
60+ loginHint = " test_user" ,
61+ oobChannel = OobChannel .PUSH
62+ )
63+
64+ assertThat(request.url(), equalTo(" https://example.okta.com/oauth2/v1/oob-authenticate" ))
65+ assertThat(request.method(), equalTo(ApiRequestMethod .POST ))
66+ assertThat(request.contentType(), equalTo(" application/x-www-form-urlencoded" ))
67+ assertThat(request.headers(), equalTo(mapOf (" Accept" to listOf (" application/json" ))))
68+ assertThat(request.query(), nullValue())
69+
70+ val formParameters = request.formParameters()
71+ assertThat(formParameters[" client_id" ], equalTo(listOf (" test_client_id" )))
72+ assertThat(formParameters[" login_hint" ], equalTo(listOf (" test_user" )))
73+ assertThat(formParameters[" channel_hint" ], equalTo(listOf (" push" )))
74+ assertThat(formParameters.containsKey(" client_secret" ), equalTo(false ))
75+ }
76+
77+ @Test
78+ fun oobAuthenticateRequest_WithCustomParameters () {
79+ val customContext = context.copy(
80+ authorizationServerId = " aus_test_id" ,
81+ clientSecret = " test_client_secret" ,
82+ additionalParameters = mapOf (" custom" to " value" )
83+ )
84+
85+ val request = DirectAuthOobAuthenticateRequest (
86+ context = customContext,
87+ loginHint = " test_user" ,
88+ oobChannel = OobChannel .SMS
89+ )
90+
91+ assertThat(request.url(), equalTo(" https://example.okta.com/oauth2/aus_test_id/v1/oob-authenticate" ))
92+ assertThat(request.query(), equalTo(mapOf (" custom" to " value" )))
93+
94+ val formParameters = request.formParameters()
95+ assertThat(formParameters[" client_id" ], equalTo(listOf (" test_client_id" )))
96+ assertThat(formParameters[" login_hint" ], equalTo(listOf (" test_user" )))
97+ assertThat(formParameters[" channel_hint" ], equalTo(listOf (" sms" )))
98+ assertThat(formParameters[" client_secret" ], equalTo(listOf (" test_client_secret" )))
99+ }
100+
101+ @Test
102+ fun oobAuthenticateRequest_returnsOobAuthenticateStateOnSuccess () = runTest {
103+ val request = DirectAuthOobAuthenticateRequest (context, " test_user" , OobChannel .PUSH )
104+ val apiResponse = KtorHttpExecutor (HttpClient (oobAuthenticateResponseMockEngine)).execute(request).getOrThrow()
105+
106+ val state = apiResponse.oobResponseAsState(context)
107+
108+ assertThat(state, instanceOf(DirectAuthenticationState .OobAuthenticate ::class .java))
109+ val oobState = state as DirectAuthenticationState .OobAuthenticate
110+ assertThat(oobState.pollContext.oobCode, equalTo(" example_oob_code" ))
111+ assertThat(oobState.pollContext.channel, equalTo(OobChannel .PUSH .value))
112+ assertThat(oobState.pollContext.expiresIn, equalTo(120 ))
113+ assertThat(oobState.pollContext.interval, equalTo(5 ))
114+ assertThat(oobState.pollContext.bindingMethod, equalTo(" none" ))
115+ assertThat(oobState.pollContext.bindingCode, nullValue())
116+ }
117+
118+ @Test
119+ fun oobAuthenticateRequest_returnsOauth2ErrorStateOnApiError () = runTest {
120+ val request = DirectAuthOobAuthenticateRequest (context, " test_user" , OobChannel .PUSH )
121+ val apiResponse = KtorHttpExecutor (HttpClient (oobAuthenticateOauth2ErrorMockEngine)).execute(request).getOrThrow()
122+
123+ val state = apiResponse.oobResponseAsState(context)
124+
125+ assertThat(state, instanceOf(DirectAuthenticationError .HttpError .Oauth2Error ::class .java))
126+ val errorState = state as DirectAuthenticationError .HttpError .Oauth2Error
127+ assertThat(errorState.error, equalTo(" invalid_request" ))
128+ assertThat(errorState.errorDescription, equalTo(" abc is not a valid channel hint" ))
129+ }
130+
131+ @Test
132+ fun oobAuthenticateRequest_returnsApiErrorStateOnServerError () = runTest {
133+ val request = DirectAuthOobAuthenticateRequest (context, " test_user" , OobChannel .PUSH )
134+ val apiResponse = KtorHttpExecutor (HttpClient (serverErrorMockEngine)).execute(request).getOrThrow()
135+
136+ val state = apiResponse.oobResponseAsState(context)
137+
138+ assertThat(state, instanceOf(DirectAuthenticationError .HttpError .ApiError ::class .java))
139+ val errorState = state as DirectAuthenticationError .HttpError .ApiError
140+ assertThat(errorState.errorCode, equalTo(" E00000" ))
141+ assertThat(errorState.errorSummary, equalTo(" Internal Server Error" ))
142+ assertThat(errorState.httpStatusCode, equalTo(HttpStatusCode .InternalServerError ))
143+ }
144+
145+ @Test
146+ fun oobAuthenticateRequest_returnsInternalErrorOnUnsupportedContentType () = runTest {
147+ val request = DirectAuthOobAuthenticateRequest (context, " test_user" , OobChannel .PUSH )
148+ val apiResponse = KtorHttpExecutor (HttpClient (notJsonMockEngine)).execute(request).getOrThrow()
149+
150+ val state = apiResponse.oobResponseAsState(context)
151+
152+ assertThat(state, instanceOf(DirectAuthenticationError .InternalError ::class .java))
153+ val error = state as DirectAuthenticationError .InternalError
154+ assertThat(error.errorCode, equalTo(UNSUPPORTED_CONTENT_TYPE ))
155+ assertThat(error.throwable, instanceOf(IllegalStateException ::class .java))
156+ assertThat(error.throwable.message, equalTo(" Unsupported content type: text/plain" ))
157+ }
158+
159+ @Test
160+ fun oobAuthenticateRequest_unparseableClientErrorResponse () = runTest {
161+ val request = DirectAuthOobAuthenticateRequest (context, " test_user" , OobChannel .PUSH )
162+
163+ val apiResponse = KtorHttpExecutor (HttpClient (unknownJsonTypeMockEngine)).execute(request).getOrThrow()
164+ val directAuthState = apiResponse.tokenResponseAsState(context)
165+
166+ assertThat(directAuthState, instanceOf(DirectAuthenticationError .InternalError ::class .java))
167+ val error = directAuthState as DirectAuthenticationError .InternalError
168+ assertThat(error.errorCode, equalTo(INVALID_RESPONSE ))
169+ assertThat(error.throwable, instanceOf(IllegalStateException ::class .java))
170+ assertThat(error.throwable.message, equalTo(" No parsable error response body: HTTP ${HttpStatusCode .BadRequest } " ))
171+ }
172+
173+ }
0 commit comments