Skip to content

Commit 51c4cd5

Browse files
authored
Added support for PKCE extension for Authorization code grant. (#1945)
1 parent 0f2cf74 commit 51c4cd5

File tree

7 files changed

+140
-10
lines changed

7 files changed

+140
-10
lines changed

src/components/operations/operation-details/ko/runtime/authorization.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ <h3 class="pt-0">Authorization
1717
<label for="authServer" class="text-monospace form-label"
1818
data-bind="text: $component.authorizationServer().displayName"></label>
1919
</div>
20-
<div class="col-6">
20+
<div class="col-7">
2121
<div class="form-group">
2222
<select id="authServer" class="form-control"
2323
data-bind="options: $component.authorizationServer().grantTypes, value: $component.selectedGrantType, optionsCaption: 'No auth'">
@@ -30,7 +30,7 @@ <h3 class="pt-0">Authorization
3030
<div class="col-4">
3131
<label for="username" class="text-monospace form-label">Username</label>
3232
</div>
33-
<div class="col-6">
33+
<div class="col-7">
3434
<div class="form-group">
3535
<input type="text" id="username" class="form-control" data-bind="textInput: $component.username" />
3636
</div>
@@ -40,7 +40,7 @@ <h3 class="pt-0">Authorization
4040
<div class="col-4">
4141
<label for="password" class="text-monospace form-label">Password</label>
4242
</div>
43-
<div class="col-6">
43+
<div class="col-7">
4444
<div class="form-group">
4545
<input type="password" id="password" class="form-control" data-bind="textInput: $component.password" />
4646
<span class="invalid-feedback" data-bind="text: $component.authorizationError"></span>
@@ -50,7 +50,7 @@ <h3 class="pt-0">Authorization
5050
<div class="row flex flex-row">
5151
<div class="col-4">
5252
</div>
53-
<div class="col-6">
53+
<div class="col-7">
5454
<div class="form-group">
5555
<button class="button button-primary"
5656
data-bind="click: $component.authenticateOAuthWithPassword">Authorize</button>
@@ -69,7 +69,7 @@ <h3 class="pt-0">Authorization
6969
Subscription key
7070
</label>
7171
</div>
72-
<div class="col-6">
72+
<div class="col-7">
7373
<div class="form-group">
7474
<!-- ko if: $component.products() && $component.products().length > 0 -->
7575
<select id="subscriptionKey" class="form-control" data-bind="value: $component.selectedSubscriptionKey">

src/components/operations/operation-details/ko/runtime/operation-console.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ <h3>Host</h3>
4646
Hostname
4747
</label>
4848
</div>
49-
<div class="col-6">
49+
<div class="col-7">
5050
<div class="form-group">
5151
<!-- ko if: $component.hostnameSelectionEnabled -->
5252
<select id="hostname" class="form-control" data-bind="value: $component.selectedHostname">
@@ -72,7 +72,7 @@ <h3>Host</h3>
7272
Wildcard segment
7373
</label>
7474
</div>
75-
<div class="col-6">
75+
<div class="col-7">
7676
<div class="form-group">
7777
<input id="wildcardSegment" type="text" autocomplete="off" class="form-control form-control-sm"
7878
placeholder="name" spellcheck="false"
@@ -105,7 +105,7 @@ <h3>Parameters
105105
<label class="text-monospace form-label" data-bind="text: parameter.name">
106106
</label>
107107
</div>
108-
<div class="col-6">
108+
<div class="col-7">
109109
<div class="form-group">
110110
<!-- ko if: parameter.options.length > 0 -->
111111
<select class="form-control" aria-label="Parameter value"

src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ export enum GrantTypes {
273273
*/
274274
authorizationCode = "authorization_code",
275275

276+
/**
277+
* Proof Key for Code Exchange (abbreviated PKCE) is an extension to the authorization code
278+
* flow to prevent CSRF and authorization code injection attacks.
279+
*/
280+
authorizationCodeWithPkce = "authorization_code (PKCE)",
281+
276282
/**
277283
* The Client Credentials grant type is used by clients to obtain an access token outside of
278284
* the context of a user.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export interface OAuthTokenResponse {
2+
/**
3+
* Access token.
4+
*/
5+
access_token: string;
6+
7+
/**
8+
* Type of the access token, e.g. `Bearer`.
9+
*/
10+
token_type: string;
11+
12+
/**
13+
* Expiration date and time, e.g. `1663205603`
14+
*/
15+
expires_on: string;
16+
17+
/**
18+
* Base64-encoded ID token.
19+
*/
20+
id_token: string;
21+
22+
/**
23+
* Refresh token.
24+
*/
25+
refresh_token: string;
26+
}

src/models/authorizationServer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export class AuthorizationServer {
3737
case "authorizationCode":
3838
convertedResult = "authorization_code";
3939
break;
40+
case "authorizationCodeWithPkce":
41+
convertedResult = "authorization_code (PKCE)";
42+
break;
4043
case "implicit":
4144
convertedResult = "implicit";
4245
break;

src/models/knownMimeTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export enum KnownMimeTypes {
22
FormData = "multipart/form-data",
33
Json = "application/json",
4-
Xml = "text/xml"
4+
Xml = "text/xml",
5+
UrlEncodedForm = "application/x-www-form-urlencoded"
56
}

src/services/oauthService.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Utils } from "../utils";
99
import { GrantTypes } from "./../constants";
1010
import { UnauthorizedError } from "./../errors/unauthorizedError";
1111
import { BackendService } from "./backendService";
12+
import { OAuthTokenResponse } from "../contracts/oauthTokenResponse";
1213

1314

1415
export class OAuthService {
@@ -19,6 +20,25 @@ export class OAuthService {
1920
private readonly logger: Logger
2021
) { }
2122

23+
private async generateCodeChallenge(codeVerifier: string): Promise<string> {
24+
const digest = await crypto.subtle.digest("SHA-256",
25+
new TextEncoder().encode(codeVerifier));
26+
27+
return btoa(String.fromCharCode(...new Uint8Array(digest)))
28+
.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
29+
}
30+
31+
private generateRandomString(length: number): string {
32+
let text = "";
33+
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
34+
35+
for (let i = 0; i < length; i++) {
36+
text += possible.charAt(Math.floor(Math.random() * possible.length));
37+
}
38+
39+
return text;
40+
}
41+
2242
public async getAuthServer(authorizationServerId: string, openidProviderId: string): Promise<AuthorizationServer> {
2343
try {
2444
if (authorizationServerId) {
@@ -58,6 +78,11 @@ export class OAuthService {
5878
accessToken = await this.authenticateCode(backendUrl, authorizationServer);
5979
break;
6080

81+
case GrantTypes.authorizationCodeWithPkce:
82+
this.logger.trackEvent("TestConsoleOAuth", { grantType: GrantTypes.authorizationCodeWithPkce });
83+
accessToken = await this.authenticateCodeWithPkce(backendUrl, authorizationServer);
84+
break;
85+
6186
case GrantTypes.clientCredentials:
6287
this.logger.trackEvent("TestConsoleOAuth", { grantType: GrantTypes.clientCredentials });
6388
accessToken = await this.authenticateClientCredentials(backendUrl, authorizationServer, apiName);
@@ -149,8 +174,9 @@ export class OAuthService {
149174
try {
150175
window.open(oauthClient.code.getUri(), "_blank", "width=400,height=500");
151176

152-
const receiveMessage = async (event: MessageEvent) => {
177+
const receiveMessage = async (event: MessageEvent): Promise<void> => {
153178
if (!event.data["accessToken"]) {
179+
alert("Unable to authenticate due to internal error.");
154180
return;
155181
}
156182

@@ -167,6 +193,74 @@ export class OAuthService {
167193
});
168194
}
169195

196+
public async authenticateCodeWithPkce(backendUrl: string, authorizationServer: AuthorizationServer): Promise<string> {
197+
const redirectUri = `${backendUrl}/signin-oauth/code-pkce/callback/${authorizationServer.name}`;
198+
const codeVerifier = this.generateRandomString(64);
199+
const challengeMethod = crypto.subtle ? "S256" : "plain"
200+
201+
const codeChallenge = challengeMethod === "S256"
202+
? await this.generateCodeChallenge(codeVerifier)
203+
: codeVerifier
204+
205+
sessionStorage.setItem("code_verifier", codeVerifier);
206+
207+
const args = new URLSearchParams({
208+
response_type: "code",
209+
client_id: authorizationServer.clientId,
210+
code_challenge_method: challengeMethod,
211+
code_challenge: codeChallenge,
212+
redirect_uri: redirectUri,
213+
scope: authorizationServer.scopes.join(" ")
214+
});
215+
216+
return new Promise((resolve, reject) => {
217+
try {
218+
window.open(authorizationServer.authorizationEndpoint + "/?" + args, "_blank", "width=400,height=500");
219+
220+
const receiveMessage = async (event: MessageEvent): Promise<void> => {
221+
const authorizationCode = event.data["code"];
222+
223+
if (!authorizationCode) {
224+
alert("Unable to authenticate due to internal error.");
225+
return;
226+
}
227+
228+
const body = new URLSearchParams({
229+
client_id: authorizationServer.clientId,
230+
code_verifier: sessionStorage.getItem("code_verifier"),
231+
grant_type: GrantTypes.authorizationCode,
232+
redirect_uri: redirectUri,
233+
code: authorizationCode
234+
});
235+
236+
const response = await this.httpClient.send<OAuthTokenResponse>({
237+
url: authorizationServer.tokenEndpoint,
238+
method: HttpMethod.post,
239+
headers: [{ name: KnownHttpHeaders.ContentType, value: KnownMimeTypes.UrlEncodedForm }],
240+
body: body.toString()
241+
});
242+
243+
if (response.statusCode === 400) {
244+
const error = response.toText();
245+
alert(error);
246+
return;
247+
}
248+
249+
const tokenResponse = response.toObject();
250+
const accessToken = tokenResponse.access_token;
251+
const accessTokenType = tokenResponse.token_type;
252+
253+
resolve(`${Utils.toTitleCase(accessTokenType)} ${accessToken}`);
254+
};
255+
256+
window.addEventListener("message", receiveMessage, false);
257+
}
258+
catch (error) {
259+
reject(error);
260+
}
261+
});
262+
}
263+
170264
/**
171265
* Acquires access token using "client credentials" grant flow.
172266
* @param backendUrl {string} Portal backend URL.

0 commit comments

Comments
 (0)