@authlink/client
Version:
Official client SDK for integrating with the Authlink Identity Provider
198 lines (197 loc) • 7.63 kB
JavaScript
import { createCodeVerifier, createCodeChallenge, generateRandomString, createSilentIframe, listenForWebMessage, mapTokenResponse, mapAuthorizeResponse, mapOpenIdConfiguration } from "../utils";
export class IdentityProviderClient {
constructor(baseUrl) {
this.codeVerifierKey = "authlink_code_verifier";
this.stateKey = "authlink_state";
this.nonceKey = "authlink_nonce";
this.baseUrl = baseUrl;
}
async authorize(options) {
let { clientId, responseType, redirectUri, scope, codeChallenge, codeChallengeMethod, prompt, responseMode, state, nonce, loginHint, requestId } = options;
const params = new URLSearchParams();
if (clientId)
params.append("client_id", clientId);
if (responseType) {
params.append("response_type", responseType);
if (responseType.includes("code")) {
if (!codeChallenge) {
let codeVerifier = createCodeVerifier();
localStorage.setItem(this.codeVerifierKey, codeVerifier);
codeChallenge = await createCodeChallenge(codeVerifier, codeChallengeMethod ?? "S256");
}
params.append("code_challenge", codeChallenge);
params.append("code_challenge_method", codeChallengeMethod ?? "S256");
}
}
if (!nonce) {
nonce = generateRandomString(32);
localStorage.setItem(this.nonceKey, nonce);
}
params.append("nonce", nonce);
if (redirectUri)
params.append("redirect_uri", redirectUri);
if (scope)
params.append("scope", scope);
if (prompt)
params.append("prompt", prompt);
if (responseMode)
params.append("response_mode", responseMode);
if (loginHint)
params.append("login_hint", loginHint);
if (requestId)
params.append("request_id", requestId);
if (!state) {
state = generateRandomString(32);
localStorage.setItem(this.stateKey, state);
}
params.append("state", state);
const url = `${this.baseUrl}/api/connect/authorize?${params.toString()}`;
if (options.autoRedirect ?? true) {
window.location.href = url;
return;
}
return url;
}
async authorizeSilently(options) {
const state = options.state ?? generateRandomString(32);
localStorage.setItem(this.stateKey, state);
const silentOptions = {
...options,
prompt: "none",
responseMode: "web_message",
state,
autoRedirect: false
};
const url = await this.authorize(silentOptions);
const iframe = createSilentIframe(url);
try {
let json = await listenForWebMessage(state, this.baseUrl);
return mapAuthorizeResponse(json);
}
finally {
document.body.removeChild(iframe);
}
}
async token(options) {
const form = new URLSearchParams({
grant_type: options.grantType,
client_id: options.clientId
});
let codeVerifier = undefined;
if (options.grantType === "authorization_code")
codeVerifier = options.codeVerifier ?? localStorage.getItem(this.codeVerifierKey);
if (options.clientSecret)
form.append("client_secret", options.clientSecret);
if (options.redirectUri)
form.append("redirect_uri", options.redirectUri);
if (options.code)
form.append("code", options.code);
if (codeVerifier)
form.append("code_verifier", codeVerifier);
if (options.refreshToken)
form.append("refresh_token", options.refreshToken);
if (options.username)
form.append("username", options.username);
if (options.password)
form.append("password", options.password);
if (options.scope)
form.append("scope", options.scope);
if (options.deviceCode)
form.append("device_code", options.deviceCode);
if (options.clientAssertionType)
form.append("client_assertion_type", options.clientAssertionType);
if (options.clientAssertion)
form.append("client_assertion", options.clientAssertion);
if (options.audience)
form.append("audience", options.audience);
const response = await fetch(`${this.baseUrl}/api/connect/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: form
});
if (!response.ok) {
const error = await response.json();
throw new { ...error, status: response.status };
}
let json = await response.json();
return mapTokenResponse(json);
}
async logout(options) {
const params = new URLSearchParams({
client_id: options.clientId,
id_token_hint: options.idTokenHint,
post_logout_redirect_uri: options.postLogoutRedirectUri
});
if (options.state)
params.append("state", options.state);
else {
const state = generateRandomString(32);
localStorage.setItem(this.stateKey, state);
params.append("state", state);
}
if (options.sessionId)
params.append("session_id", options.sessionId);
if (options.reason)
params.append("logout_hint", options.reason);
window.location.href = `${this.baseUrl}/api/connect/logout?${params.toString()}`;
}
validateState(state) {
const storedState = localStorage.getItem(this.stateKey);
return storedState === state;
}
validateNonce(nonce) {
const storedNonce = localStorage.getItem(this.nonceKey);
return storedNonce === nonce;
}
async jwks() {
const response = await fetch(`${this.baseUrl}/api/connect/jwks`, {
method: "GET"
});
if (!response.ok) {
const error = await response.json();
throw new { ...error, status: response.status };
}
return await response.json();
}
async configuration() {
const response = await fetch(`${this.baseUrl}/.well-known/openid-configuration`, {
method: "GET"
});
if (!response.ok) {
const error = await response.json();
throw new { ...error, status: response.status };
}
let json = await response.json();
return mapOpenIdConfiguration(json);
}
async userInfo(accessToken) {
const response = await fetch("/api/connect/userinfo", {
method: "GET",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
const error = await response.json();
throw new { ...error, status: response.status };
}
return await response.json();
}
clear() {
localStorage.removeItem(this.codeVerifierKey);
localStorage.removeItem(this.stateKey);
localStorage.removeItem(this.nonceKey);
}
getCodeVerifier() {
return localStorage.getItem(this.codeVerifierKey);
}
getState() {
return localStorage.getItem(this.stateKey);
}
getNonce() {
return localStorage.getItem(this.nonceKey);
}
}