1
1
using System . Net ;
2
- using System . Net . Http . Headers ;
3
2
using System . Net . Http . Json ;
4
3
using System . Text . Json ;
5
4
using D2L . Bmx . Okta . Models ;
6
5
7
6
namespace D2L . Bmx . Okta ;
8
7
9
- internal interface IOktaApi {
10
- void SetOrganization ( string organization ) ;
11
- void AddSession ( string sessionId ) ;
8
+ internal interface IOktaClientFactory {
9
+ IOktaAnonymousClient CreateAnonymousClient ( string org ) ;
10
+ IOktaAuthenticatedClient CreateAuthenticatedClient ( string org , string sessionId ) ;
11
+ }
12
+
13
+ internal interface IOktaAnonymousClient {
12
14
Task < AuthenticateResponse > AuthenticateAsync ( string username , string password ) ;
13
15
Task IssueMfaChallengeAsync ( string stateToken , string factorId ) ;
14
16
Task < AuthenticateResponse > VerifyMfaChallengeResponseAsync (
15
17
string stateToken ,
16
18
string factorId ,
17
- string challengeResponse ) ;
19
+ string challengeResponse
20
+ ) ;
18
21
Task < OktaSession > CreateSessionAsync ( string sessionToken ) ;
19
- Task < OktaApp [ ] > GetAwsAccountAppsAsync ( ) ;
20
- Task < string > GetPageAsync ( string samlLoginUrl ) ;
21
22
}
22
23
23
- internal class OktaApi : IOktaApi {
24
- private readonly CookieContainer _cookieContainer ;
25
- private readonly HttpClient _httpClient ;
26
- private string ? _organization ;
24
+ internal interface IOktaAuthenticatedClient {
25
+ Task < OktaApp [ ] > GetAwsAccountAppsAsync ( ) ;
26
+ Task < string > GetPageAsync ( string url ) ;
27
+ }
27
28
28
- public OktaApi ( ) {
29
- _cookieContainer = new CookieContainer ( ) ;
30
- _httpClient = new HttpClient ( new HttpClientHandler { CookieContainer = _cookieContainer } ) {
31
- Timeout = TimeSpan . FromSeconds ( 30 )
29
+ internal class OktaClientFactory : IOktaClientFactory {
30
+ IOktaAnonymousClient IOktaClientFactory . CreateAnonymousClient ( string org ) {
31
+ var httpClient = new HttpClient {
32
+ Timeout = TimeSpan . FromSeconds ( 30 ) ,
33
+ BaseAddress = GetBaseAddress ( org ) ,
32
34
} ;
33
- _httpClient . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
35
+ return new OktaAnonymousClient ( httpClient ) ;
34
36
}
35
37
36
- void IOktaApi . SetOrganization ( string organization ) {
37
- _organization = organization ;
38
- if ( ! organization . Contains ( '.' ) ) {
39
- _httpClient . BaseAddress = new Uri ( $ "https://{ organization } .okta.com/api/v1/" ) ;
40
- } else {
41
- _httpClient . BaseAddress = new Uri ( $ "https://{ organization } /api/v1/" ) ;
42
- }
38
+ IOktaAuthenticatedClient IOktaClientFactory . CreateAuthenticatedClient ( string org , string sessionId ) {
39
+ var baseAddress = GetBaseAddress ( org ) ;
40
+
41
+ var cookieContainer = new CookieContainer ( ) ;
42
+ cookieContainer . Add ( new Cookie ( "sid" , sessionId , "/" , baseAddress . Host ) ) ;
43
+
44
+ var httpClient = new HttpClient ( new SocketsHttpHandler {
45
+ CookieContainer = cookieContainer ,
46
+ } ) {
47
+ Timeout = TimeSpan . FromSeconds ( 30 ) ,
48
+ BaseAddress = baseAddress ,
49
+ } ;
50
+
51
+ return new OktaAuthenticatedClient ( httpClient ) ;
43
52
}
44
53
45
- void IOktaApi . AddSession ( string sessionId ) {
46
- if ( _httpClient . BaseAddress is not null ) {
47
- _cookieContainer . Add ( new Cookie ( "sid" , sessionId , "/" , _httpClient . BaseAddress . Host ) ) ;
48
- } else {
49
- throw new InvalidOperationException ( "Error adding session: http client base address is not defined" ) ;
50
- }
54
+ private static Uri GetBaseAddress ( string org ) {
55
+ return org . Contains ( '.' )
56
+ ? new Uri ( $ "https://{ org } /api/v1/" )
57
+ : new Uri ( $ "https://{ org } .okta.com/api/v1/" ) ;
51
58
}
59
+ }
52
60
53
- async Task < AuthenticateResponse > IOktaApi . AuthenticateAsync ( string username , string password ) {
61
+ internal class OktaAnonymousClient ( HttpClient httpClient ) : IOktaAnonymousClient {
62
+ async Task < AuthenticateResponse > IOktaAnonymousClient . AuthenticateAsync ( string username , string password ) {
54
63
HttpResponseMessage resp ;
55
64
try {
56
- resp = await _httpClient . PostAsJsonAsync (
65
+ resp = await httpClient . PostAsJsonAsync (
57
66
"authn" ,
58
67
new AuthenticateRequest ( username , password ) ,
59
- JsonCamelCaseContext . Default . AuthenticateRequest ) ;
68
+ JsonCamelCaseContext . Default . AuthenticateRequest
69
+ ) ;
60
70
} catch ( Exception ex ) {
61
71
throw new BmxException ( "Okta authentication request failed." , ex ) ;
62
72
}
@@ -65,7 +75,8 @@ async Task<AuthenticateResponse> IOktaApi.AuthenticateAsync( string username, st
65
75
try {
66
76
authnResponse = await JsonSerializer . DeserializeAsync (
67
77
await resp . Content . ReadAsStreamAsync ( ) ,
68
- JsonCamelCaseContext . Default . AuthenticateResponseRaw ) ;
78
+ JsonCamelCaseContext . Default . AuthenticateResponseRaw
79
+ ) ;
69
80
} catch ( Exception ex ) {
70
81
throw new BmxException ( "Okta authentication failed. Okta returned an invalid response" , ex ) ;
71
82
}
@@ -87,16 +98,36 @@ await resp.Content.ReadAsStreamAsync(),
87
98
) ;
88
99
}
89
100
90
- string org = _organization ?? "unknown" ;
91
- throw new BmxException ( $ """
92
- Okta authentication for user '{ username } ' in org '{ org } ' failed.
93
- Check if org, user, and password is correct.
94
- """ ) ;
101
+ return new AuthenticateResponse . Failure ( resp . StatusCode ) ;
95
102
}
96
103
97
- async Task IOktaApi . IssueMfaChallengeAsync ( string stateToken , string factorId ) {
104
+ async Task < OktaSession > IOktaAnonymousClient . CreateSessionAsync ( string sessionToken ) {
105
+ HttpResponseMessage resp ;
98
106
try {
99
- var response = await _httpClient . PostAsJsonAsync (
107
+ resp = await httpClient . PostAsJsonAsync (
108
+ "sessions" ,
109
+ new CreateSessionRequest ( sessionToken ) ,
110
+ JsonCamelCaseContext . Default . CreateSessionRequest ) ;
111
+ resp . EnsureSuccessStatusCode ( ) ;
112
+ } catch ( Exception ex ) {
113
+ throw new BmxException ( "Request to create Okta Session failed." , ex ) ;
114
+ }
115
+
116
+ OktaSession ? session ;
117
+ try {
118
+ session = await JsonSerializer . DeserializeAsync (
119
+ await resp . Content . ReadAsStreamAsync ( ) ,
120
+ JsonCamelCaseContext . Default . OktaSession ) ;
121
+ } catch ( Exception ex ) {
122
+ throw new BmxException ( "Error creating Okta Session. Okta returned an invalid response" , ex ) ;
123
+ }
124
+
125
+ return session ?? throw new BmxException ( "Error creating Okta Session." ) ;
126
+ }
127
+
128
+ async Task IOktaAnonymousClient . IssueMfaChallengeAsync ( string stateToken , string factorId ) {
129
+ try {
130
+ var response = await httpClient . PostAsJsonAsync (
100
131
$ "authn/factors/{ factorId } /verify",
101
132
new IssueMfaChallengeRequest ( stateToken ) ,
102
133
JsonCamelCaseContext . Default . IssueMfaChallengeRequest ) ;
@@ -107,7 +138,7 @@ async Task IOktaApi.IssueMfaChallengeAsync( string stateToken, string factorId )
107
138
}
108
139
}
109
140
110
- async Task < AuthenticateResponse > IOktaApi . VerifyMfaChallengeResponseAsync (
141
+ async Task < AuthenticateResponse > IOktaAnonymousClient . VerifyMfaChallengeResponseAsync (
111
142
string stateToken ,
112
143
string factorId ,
113
144
string challengeResponse
@@ -117,7 +148,7 @@ string challengeResponse
117
148
PassCode : challengeResponse ) ;
118
149
HttpResponseMessage resp ;
119
150
try {
120
- resp = await _httpClient . PostAsJsonAsync (
151
+ resp = await httpClient . PostAsJsonAsync (
121
152
$ "authn/factors/{ factorId } /verify",
122
153
request ,
123
154
JsonCamelCaseContext . Default . VerifyMfaChallengeResponseRequest ) ;
@@ -137,37 +168,15 @@ await resp.Content.ReadAsStreamAsync(),
137
168
if ( authnResponse ? . SessionToken is not null ) {
138
169
return new AuthenticateResponse . Success ( authnResponse . SessionToken ) ;
139
170
}
140
- throw new BmxException ( "Error verifying MFA with Okta." ) ;
141
- }
142
-
143
- async Task < OktaSession > IOktaApi . CreateSessionAsync ( string sessionToken ) {
144
- HttpResponseMessage resp ;
145
- try {
146
- resp = await _httpClient . PostAsJsonAsync (
147
- "sessions" ,
148
- new CreateSessionRequest ( sessionToken ) ,
149
- JsonCamelCaseContext . Default . CreateSessionRequest ) ;
150
- resp . EnsureSuccessStatusCode ( ) ;
151
- } catch ( Exception ex ) {
152
- throw new BmxException ( "Request to create Okta Session failed." , ex ) ;
153
- }
154
-
155
- OktaSession ? session ;
156
- try {
157
- session = await JsonSerializer . DeserializeAsync (
158
- await resp . Content . ReadAsStreamAsync ( ) ,
159
- JsonCamelCaseContext . Default . OktaSession ) ;
160
- } catch ( Exception ex ) {
161
- throw new BmxException ( "Error creating Okta Session. Okta returned an invalid response" , ex ) ;
162
- }
163
-
164
- return session ?? throw new BmxException ( "Error creating Okta Session." ) ;
171
+ return new AuthenticateResponse . Failure ( resp . StatusCode ) ;
165
172
}
173
+ }
166
174
167
- async Task < OktaApp [ ] > IOktaApi . GetAwsAccountAppsAsync ( ) {
175
+ internal class OktaAuthenticatedClient ( HttpClient httpClient ) : IOktaAuthenticatedClient {
176
+ async Task < OktaApp [ ] > IOktaAuthenticatedClient . GetAwsAccountAppsAsync ( ) {
168
177
OktaApp [ ] ? apps ;
169
178
try {
170
- apps = await _httpClient . GetFromJsonAsync (
179
+ apps = await httpClient . GetFromJsonAsync (
171
180
"users/me/appLinks" ,
172
181
JsonCamelCaseContext . Default . OktaAppArray ) ;
173
182
} catch ( Exception ex ) {
@@ -178,7 +187,7 @@ async Task<OktaApp[]> IOktaApi.GetAwsAccountAppsAsync() {
178
187
?? throw new BmxException ( "Error retrieving AWS accounts from Okta." ) ;
179
188
}
180
189
181
- async Task < string > IOktaApi . GetPageAsync ( string samlLoginUrl ) {
182
- return await _httpClient . GetStringAsync ( samlLoginUrl ) ;
190
+ async Task < string > IOktaAuthenticatedClient . GetPageAsync ( string url ) {
191
+ return await httpClient . GetStringAsync ( url ) ;
183
192
}
184
193
}
0 commit comments