Skip to content

Commit

Permalink
Upgrade @oslojs/oauth2 dependency (#108)
Browse files Browse the repository at this point in the history
Upgrade the doc to use the latest version of Oslo, copy parts of old
versions of Oslo to this library to keep the compatibility while using
the new version of Oslo
  • Loading branch information
sergiodxa authored Aug 6, 2024
1 parent 4de64c4 commit 368e877
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 53 deletions.
Binary file modified bun.lockb
Binary file not shown.
23 changes: 7 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,8 @@
"version": "2.1.0",
"description": "A strategy to use and implement OAuth2 framework for authentication with federated services like Google, Facebook, GitHub, etc.",
"license": "MIT",
"funding": [
"https://github.com/sponsors/sergiodxa"
],
"keywords": [
"remix",
"remix-auth",
"auth",
"authentication",
"strategy"
],
"funding": ["https://github.com/sponsors/sergiodxa"],
"keywords": ["remix", "remix-auth", "auth", "authentication", "strategy"],
"author": {
"name": "Sergio Xalambrí",
"email": "[email protected]",
Expand All @@ -31,23 +23,22 @@
"typecheck": "tsc --noEmit",
"quality": "biome check .",
"quality:fix": "biome check . --apply-unsafe",
"exports": "bun run ./scripts/exports.ts"
"exports": "bun run ./scripts/exports.ts",
"unused": "knip"
},
"sideEffects": false,
"type": "module",
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=20.0.0"
},
"files": [
"build",
"package.json",
"README.md"
],
"files": ["build", "package.json", "README.md"],
"exports": {
".": "./build/index.js",
"./package.json": "./package.json"
},
"dependencies": {
"@oslojs/crypto": "^0.6.2",
"@oslojs/encoding": "^0.4.1",
"@oslojs/oauth2": "^0.5.0",
"debug": "^4.3.4"
},
Expand Down
56 changes: 23 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
import {
AuthorizationCodeAccessTokenRequestContext,
AuthorizationCodeAuthorizationURL,
OAuth2RequestError,
RefreshRequestContext,
TokenResponseBody,
TokenRevocationRequestContext,
generateCodeVerifier,
generateState,
sendTokenRequest,
sendTokenRevocationRequest,
} from "@oslojs/oauth2";
import {
AppLoadContext,
SessionStorage,
Expand All @@ -21,6 +9,10 @@ import {
Strategy,
StrategyVerifyCallback,
} from "remix-auth";
import { AuthorizationCode } from "./lib/authorization-code.js";
import { Generator } from "./lib/generator.js";
import { OAuth2Request } from "./lib/request.js";
import { Token } from "./lib/token.js";

let debug = createDebug("OAuth2Strategy");

Expand Down Expand Up @@ -103,7 +95,7 @@ export interface OAuth2StrategyVerifyParams<
Profile extends OAuth2Profile,
ExtraTokenParams extends Record<string, unknown> = Record<string, never>,
> {
tokens: TokenResponseBody & ExtraTokenParams;
tokens: Token.Response.Body & ExtraTokenParams;
profile: Profile;
request: Request;
context?: AppLoadContext;
Expand Down Expand Up @@ -168,17 +160,17 @@ export class OAuth2Strategy<
if (!stateUrl) {
debug("No state found in the URL, redirecting to authorization endpoint");

let state = generateState();
let state = Generator.state();
session.set(this.sessionStateKey, state);

debug("State", state);

let codeVerifier = generateCodeVerifier();
let codeVerifier = Generator.codeVerifier();
session.set(this.sessionCodeVerifierKey, codeVerifier);

debug("Code verifier", codeVerifier);

let authorizationURL = new AuthorizationCodeAuthorizationURL(
let authorizationURL = new AuthorizationCode.AuthorizationURL(
this.options.authorizationEndpoint.toString(),
this.options.clientId,
);
Expand All @@ -187,7 +179,7 @@ export class OAuth2Strategy<
authorizationURL.setState(state);

if (this.options.scopes)
authorizationURL.appendScopes(...this.options.scopes);
authorizationURL.addScopes(...this.options.scopes);

if (this.options.codeChallengeMethod === "S256") {
authorizationURL.setS256CodeChallenge(codeVerifier);
Expand Down Expand Up @@ -265,7 +257,7 @@ export class OAuth2Strategy<

try {
debug("Validating authorization code");
let context = new AuthorizationCodeAccessTokenRequestContext(code);
let context = new Token.Request.Context(code);

context.setRedirectURI(this.options.redirectURI.toString());
context.setCodeVerifier(codeVerifier);
Expand All @@ -282,7 +274,7 @@ export class OAuth2Strategy<
);
}

let tokens = await sendTokenRequest<TokenResponseBody & ExtraParams>(
let tokens = await Token.Request.send<ExtraParams>(
this.options.tokenEndpoint.toString(),
context,
{ signal: request.signal },
Expand Down Expand Up @@ -334,7 +326,7 @@ export class OAuth2Strategy<
}
}

protected async userProfile(tokens: TokenResponseBody): Promise<Profile> {
protected async userProfile(tokens: Token.Response.Body): Promise<Profile> {
return { provider: "oauth2" } as Profile;
}

Expand All @@ -349,7 +341,7 @@ export class OAuth2Strategy<
*/
protected authorizationParams(
params: URLSearchParams,
request?: Request,
request: Request,
): URLSearchParams {
return new URLSearchParams(params);
}
Expand All @@ -368,9 +360,9 @@ export class OAuth2Strategy<
) {
let scopes = options.scopes ?? this.options.scopes ?? [];

let context = new RefreshRequestContext(refreshToken);
let context = new Token.RefreshRequest.Context(refreshToken);

context.appendScopes(...scopes);
context.addScopes(...scopes);

if (this.options.authenticateWith === "http_basic_auth") {
context.authenticateWithHTTPBasicAuth(
Expand All @@ -384,14 +376,14 @@ export class OAuth2Strategy<
);
}

return sendTokenRequest<TokenResponseBody & ExtraParams>(
return Token.Request.send<ExtraParams>(
this.options.tokenEndpoint.toString(),
context,
{ signal: options.signal },
);
}

public revokeToken(
public async revokeToken(
token: string,
options: {
signal?: AbortSignal;
Expand All @@ -402,11 +394,9 @@ export class OAuth2Strategy<
throw new Error("Token revocation endpoint is not set");
}

let context = new TokenRevocationRequestContext(token);
let context = new Token.RevocationRequest.Context(token);

if (options.tokenType) {
context.setTokenTypeHint(options.tokenType);
}
if (options.tokenType) context.setTokenTypeHint(options.tokenType);

if (this.options.authenticateWith === "http_basic_auth") {
context.authenticateWithHTTPBasicAuth(
Expand All @@ -420,8 +410,8 @@ export class OAuth2Strategy<
);
}

return sendTokenRevocationRequest(
this.options.tokenRevocationEndpoint.toString(),
await Token.RevocationRequest.send(
this.options.tokenRevocationEndpoint,
context,
{ signal: options.signal },
);
Expand Down Expand Up @@ -449,5 +439,5 @@ export class OAuth2Error extends Error {
}
}

export { OAuth2RequestError };
export type { TokenResponseBody } from "@oslojs/oauth2";
export const OAuth2RequestError = OAuth2Request.Error;
export type TokenResponseBody = Token.Response.Body;
48 changes: 48 additions & 0 deletions src/lib/authorization-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* A lot of the code here was originally implemented by @pilcrowOnPaper for a
* previous version of `@oslojs/oauth2`, as Pilcrow decided to change the
* direction of the library to focus on response parsing, I decided to copy the
* old code and adapt it to the new structure of the library.
*/
import { sha256 } from "@oslojs/crypto/sha2";
import { encodeBase64urlNoPadding } from "@oslojs/encoding";

export namespace AuthorizationCode {
export class AuthorizationURL extends URL {
constructor(authorizationEndpoint: string, clientId: string) {
super(authorizationEndpoint);
this.searchParams.set("response_type", "code");
this.searchParams.set("client_id", clientId);
}

public setRedirectURI(redirectURI: string): void {
this.searchParams.set("redirect_uri", redirectURI);
}

public addScopes(...scopes: string[]): void {
if (scopes.length < 1) {
return;
}
let scopeValue = scopes.join(" ");
const existingScopes = this.searchParams.get("scope");
if (existingScopes !== null) scopeValue = ` ${existingScopes}`;
this.searchParams.set("scope", scopeValue);
}

public setState(state: string): void {
this.searchParams.set("state", state);
}

public setS256CodeChallenge(codeVerifier: string): void {
const codeChallengeBytes = sha256(new TextEncoder().encode(codeVerifier));
const codeChallenge = encodeBase64urlNoPadding(codeChallengeBytes);
this.searchParams.set("code_challenge", codeChallenge);
this.searchParams.set("code_challenge_method", "S256");
}

public setPlainCodeChallenge(codeVerifier: string): void {
this.searchParams.set("code_challenge", codeVerifier);
this.searchParams.set("code_challenge_method", "plain");
}
}
}
21 changes: 21 additions & 0 deletions src/lib/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* A lot of the code here was originally implemented by @pilcrowOnPaper for a
* previous version of `@oslojs/oauth2`, as Pilcrow decided to change the
* direction of the library to focus on response parsing, I decided to copy the
* old code and adapt it to the new structure of the library.
*/
import { encodeBase64urlNoPadding } from "@oslojs/encoding";

export namespace Generator {
export function codeVerifier(): string {
const randomValues = new Uint8Array(32);
crypto.getRandomValues(randomValues);
return encodeBase64urlNoPadding(randomValues);
}

export function state(): string {
const randomValues = new Uint8Array(32);
crypto.getRandomValues(randomValues);
return encodeBase64urlNoPadding(randomValues);
}
}
76 changes: 76 additions & 0 deletions src/lib/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* A lot of the code here was originally implemented by @pilcrowOnPaper for a
* previous version of `@oslojs/oauth2`, as Pilcrow decided to change the
* direction of the library to focus on response parsing, I decided to copy the
* old code and adapt it to the new structure of the library.
*/
import { encodeBase64 } from "@oslojs/encoding";

export namespace OAuth2Request {
export abstract class Context {
public method: string;
public body = new URLSearchParams();
public headers = new Headers();

constructor(method: string) {
this.method = method;
this.headers.set("Content-Type", "application/x-www-form-urlencoded");
this.headers.set("Accept", "application/json");
this.headers.set("User-Agent", "oslo");
}

public setClientId(clientId: string): void {
this.body.set("client_id", clientId);
}

public authenticateWithRequestBody(
clientId: string,
clientSecret: string,
): void {
this.setClientId(clientId);
this.body.set("client_secret", clientSecret);
}

public authenticateWithHTTPBasicAuth(
clientId: string,
clientSecret: string,
): void {
const authorizationHeader = `Basic ${encodeBase64(
new TextEncoder().encode(`${clientId}:${clientSecret}`),
)}`;
this.headers.set("Authorization", authorizationHeader);
}

toRequest(url: ConstructorParameters<URL>["0"]) {
return new Request(url, {
method: this.method,
body: this.body,
headers: this.headers,
});
}
}

// biome-ignore lint/suspicious/noShadowRestrictedNames: It's namespaced
export class Error extends globalThis.Error {
public request: Request;
public context: OAuth2Request.Context;
public description: string | null;
public uri: string | null;
public responseHeaders: Headers;

constructor(
message: string,
request: Request,
context: OAuth2Request.Context,
responseHeaders: Headers,
options?: { description?: string; uri?: string },
) {
super(message);
this.request = request;
this.context = context;
this.responseHeaders = responseHeaders;
this.description = options?.description ?? null;
this.uri = options?.uri ?? null;
}
}
}
Loading

0 comments on commit 368e877

Please sign in to comment.