Fix PKCE flow and clean up other flows

This commit is contained in:
Gregory Schier
2025-06-25 07:10:11 -07:00
parent f476d87613
commit 8817be679b
10 changed files with 143 additions and 107 deletions

View File

@@ -1,8 +1,9 @@
import { Context } from '@yaakapp/api';
import type { Context } from '@yaakapp/api';
import { createHash, randomBytes } from 'node:crypto';
import { getAccessToken } from '../getAccessToken';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import { AccessToken, getDataDirKey, storeToken } from '../store';
import type { AccessToken } from '../store';
import { getDataDirKey, storeToken } from '../store';
export const PKCE_SHA256 = 'S256';
export const PKCE_PLAIN = 'plain';
@@ -34,8 +35,8 @@ export async function getAuthorizationCode(
audience: string | null;
credentialsInBody: boolean;
pkce: {
challengeMethod: string | null;
codeVerifier: string | null;
challengeMethod: string;
codeVerifier: string;
} | null;
tokenName: 'access_token' | 'id_token';
},
@@ -59,26 +60,25 @@ export async function getAuthorizationCode(
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
authorizationUrl.searchParams.set(
'code_challenge',
createPkceCodeChallenge(verifier, challengeMethod),
pkceCodeChallenge(pkce.codeVerifier, pkce.challengeMethod),
);
authorizationUrl.searchParams.set('code_challenge_method', challengeMethod);
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
}
return new Promise(async (resolve, reject) => {
const authorizationUrlStr = authorizationUrl.toString();
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
console.log('[oauth2] Authorizing', authorizationUrlStr);
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
const dataDirKey = await getDataDirKey(ctx, contextId);
const authorizationUrlStr = authorizationUrl.toString();
console.log('[oauth2] Authorizing', authorizationUrlStr);
// eslint-disable-next-line no-async-promise-executor
const code = await new Promise<string>(async (resolve, reject) => {
let foundCode = false;
let { close } = await ctx.window.openUrl({
const { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: 'oauth-authorization-url',
dataDirKey: await getDataDirKey(ctx, contextId),
dataDirKey,
async onClose() {
if (!foundCode) {
reject(new Error('Authorization window closed'));
@@ -89,6 +89,7 @@ export async function getAuthorizationCode(
if (logsEnabled) console.log('[oauth2] Navigated to', urlStr);
if (url.searchParams.has('error')) {
close();
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`));
}
@@ -101,37 +102,35 @@ export async function getAuthorizationCode(
// Close the window here, because we don't need it anymore!
foundCode = true;
close();
console.log('[oauth2] Code found');
const response = await getAccessToken(ctx, {
grantType: 'authorization_code',
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: 'code', value: code },
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
],
});
try {
resolve(await storeToken(ctx, contextId, response, tokenName));
} catch (err) {
reject(err);
}
resolve(code);
},
});
});
console.log('[oauth2] Code found');
const response = await fetchAccessToken(ctx, {
grantType: 'authorization_code',
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: 'code', value: code },
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
],
});
return storeToken(ctx, contextId, response, tokenName);
}
function createPkceCodeVerifier() {
export function genPkceCodeVerifier() {
return encodeForPkce(randomBytes(32));
}
function createPkceCodeChallenge(verifier: string, method: string) {
function pkceCodeChallenge(verifier: string, method: string) {
if (method === 'plain') {
return verifier;
}

View File

@@ -1,5 +1,6 @@
import { Context } from '@yaakapp/api';
import { getAccessToken } from '../getAccessToken';
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import { getToken, storeToken } from '../store';
export async function getClientCredentials(
@@ -22,13 +23,11 @@ export async function getClientCredentials(
},
) {
const token = await getToken(ctx, contextId);
if (token) {
// resolve(token.response.access_token);
// TODO: Refresh token if expired
// return;
if (token && !isTokenExpired(token)) {
return token;
}
const response = await getAccessToken(ctx, {
const response = await fetchAccessToken(ctx, {
grantType: 'client_credentials',
accessTokenUrl,
audience,

View File

@@ -1,7 +1,9 @@
import { Context } from '@yaakapp/api';
import { AccessToken, AccessTokenRawResponse, getToken, storeToken } from '../store';
import type { Context } from '@yaakapp/api';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import { getToken, storeToken } from '../store';
export function getImplicit(
export async function getImplicit(
ctx: Context,
contextId: string,
{
@@ -24,31 +26,30 @@ export function getImplicit(
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId);
if (token) {
// resolve(token.response.access_token);
// TODO: Refresh token if expired
// return;
}
const token = await getToken(ctx, contextId);
if (token != null && !isTokenExpired(token)) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
authorizationUrl.searchParams.set('response_type', 'token');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (responseType.includes('id_token')) {
authorizationUrl.searchParams.set(
'nonce',
String(Math.floor(Math.random() * 9999999999999) + 1),
);
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
authorizationUrl.searchParams.set('response_type', 'token');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (responseType.includes('id_token')) {
authorizationUrl.searchParams.set(
'nonce',
String(Math.floor(Math.random() * 9999999999999) + 1),
);
}
const authorizationUrlStr = authorizationUrl.toString();
// eslint-disable-next-line no-async-promise-executor
const newToken = await new Promise<AccessToken>(async (resolve, reject) => {
let foundAccessToken = false;
let { close } = await ctx.window.openUrl({
const authorizationUrlStr = authorizationUrl.toString();
const { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: 'oauth-authorization-url',
async onClose() {
@@ -76,11 +77,13 @@ export function getImplicit(
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
try {
resolve(await storeToken(ctx, contextId, response));
resolve(storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
},
});
});
return newToken;
}

View File

@@ -1,7 +1,8 @@
import { Context } from '@yaakapp/api';
import { getAccessToken } from '../getAccessToken';
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import { AccessToken, storeToken } from '../store';
import type { AccessToken} from '../store';
import { storeToken } from '../store';
export async function getPassword(
ctx: Context,
@@ -37,7 +38,7 @@ export async function getPassword(
return token;
}
const response = await getAccessToken(ctx, {
const response = await fetchAccessToken(ctx, {
accessTokenUrl,
clientId,
clientSecret,