generated from AdobeDocs/dev-site-documentation-template
-
Notifications
You must be signed in to change notification settings - Fork 8
/
OAuthUtils.js
148 lines (124 loc) · 4.93 KB
/
OAuthUtils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/*
Copyright 2023 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
import { OAUTH_HASH_ALGORITHM } from "../constants.js";
export class OAuthUtils {
_store;
constructor(store) {
this._store = store;
}
/**
* Generate OAuth 2.0 PKCE challenge parameters which can be used to sign in a user.
* @returns Response containing OAuth 2.0 PKCE challenger parameters.
*/
async generateChallenge() {
const codeVerifier = getUniqueString(64);
const codeChallenge = await getCodeChallenge(codeVerifier);
return {
codeChallenge,
codeVerifier
};
}
/**
* Generate access token which can be used to access the OAuth 2.0 backed services.
* @param tokenRequest - Payload with the parameters to be used for generating access and refresh tokens.
*/
async generateAccessToken(tokenRequest) {
const formData = new FormData();
formData.append("client_id", tokenRequest.clientId);
formData.append("grant_type", "authorization_code");
formData.append("code", tokenRequest.code);
formData.append("redirect_uri", tokenRequest.redirectUri);
formData.append("code_verifier", tokenRequest.codeVerifier);
await saveTokenResponse(
this._store,
tokenRequest.id,
tokenRequest.clientId,
formData,
tokenRequest.tokenUrl
);
}
/**
* Get the generated access token to be used for accessing the OAuth 2.0 backed services.
* The access token returned is always valid (i.e. not expired).
* Upon expiration of the current access token, the refresh token is used to generate a new access token.
* @param id - A unique value associated with each authentication request.
* @returns Access token as a string.
*/
async getAccessToken(id) {
const tokenResponseString = await this._store.getItem(id);
if (!tokenResponseString) {
throw new Error(`No token has been generated for request id: ${id}.`);
}
const tokenResponse = JSON.parse(tokenResponseString);
if (!tokenResponse) {
throw new Error(`Invalid token object found for request id: ${id}.`);
}
const currentTime = Date.now();
if (currentTime < tokenResponse.expiry) {
return tokenResponse.accessToken;
}
const formData = new FormData();
formData.append("client_id", tokenResponse.clientId);
formData.append("grant_type", "refresh_token");
formData.append("refresh_token", tokenResponse.refreshToken);
const { accessToken } = await saveTokenResponse(
id,
tokenResponse.clientId,
formData,
tokenResponse.tokenUrl
);
return accessToken;
}
}
function getUniqueString(length) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
async function getCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const encodedCodeVerifier = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest(OAUTH_HASH_ALGORITHM, encodedCodeVerifier);
return base64UrlEncode(digest);
}
function base64UrlEncode(digest) {
let result = "";
const bytes = new Uint8Array(digest);
for (var i = 0; i < bytes.byteLength; i++) {
result += String.fromCharCode(bytes[i]);
}
return btoa(result).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
async function saveTokenResponse(store, id, clientId, formData, tokenUrl) {
const options = {
method: "POST",
body: formData
};
const response = await fetch(tokenUrl, options);
if (!response.ok) {
throw new Error(`Failed to get the access token for request id: ${id}.`);
}
const data = await response.json();
const tokenResponse = {
clientId,
tokenUrl,
accessToken: data.access_token,
refreshToken: data.refresh_token ?? formData.get("refresh_token"),
// Keep a buffer of additional 60 seconds
expiry: Date.now() + (data.expires_in - 60) * 1000
};
await store.setItem(id, JSON.stringify(tokenResponse));
return tokenResponse;
}