feat: QoL improvements for OAuth Callback

This commit is contained in:
Maxwell Gerber
2025-04-07 14:53:16 -07:00
parent 4053aa122d
commit 3f9500f954
4 changed files with 207 additions and 52 deletions

View File

@@ -0,0 +1,78 @@
import {
generateOAuthErrorDescription,
parseOAuthCallbackParams,
} from "@/utils/oauthUtils.ts";
describe("parseOAuthCallbackParams", () => {
it("Returns successful: true and code when present", () => {
expect(parseOAuthCallbackParams("?code=fake-code")).toEqual({
successful: true,
code: "fake-code",
});
});
it("Returns successful: false and error when error is present", () => {
expect(parseOAuthCallbackParams("?error=access_denied")).toEqual({
successful: false,
error: "access_denied",
error_description: null,
error_uri: null,
});
});
it("Returns optional error metadata fields when present", () => {
const search =
"?error=access_denied&" +
"error_description=User%20Denied%20Request&" +
"error_uri=https%3A%2F%2Fexample.com%2Ferror-docs";
expect(parseOAuthCallbackParams(search)).toEqual({
successful: false,
error: "access_denied",
error_description: "User Denied Request",
error_uri: "https://example.com/error-docs",
});
});
it("Returns error when nothing present", () => {
expect(parseOAuthCallbackParams("?")).toEqual({
successful: false,
error: "invalid_request",
error_description: "Missing code or error in response",
error_uri: null,
});
});
});
describe("generateOAuthErrorDescription", () => {
it("When only error is present", () => {
expect(
generateOAuthErrorDescription({
successful: false,
error: "invalid_request",
error_description: null,
error_uri: null,
}),
).toBe("Error: invalid_request.");
});
it("When error description is present", () => {
expect(
generateOAuthErrorDescription({
successful: false,
error: "invalid_request",
error_description: "The request could not be completed as dialed",
error_uri: null,
}),
).toEqual(
"Error: invalid_request.\nDetails: The request could not be completed as dialed.",
);
});
it("When all fields present", () => {
expect(
generateOAuthErrorDescription({
successful: false,
error: "invalid_request",
error_description: "The request could not be completed as dialed",
error_uri: "https://example.com/error-docs",
}),
).toEqual(
"Error: invalid_request.\nDetails: The request could not be completed as dialed.\nMore info: https://example.com/error-docs.",
);
});
});

View File

@@ -0,0 +1,65 @@
// The parsed query parameters returned by the Authorization Server
// representing either a valid authorization_code or an error
// ref: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-4.1.2
type CallbackParams =
| {
successful: true;
// The authorization code is generated by the authorization server.
code: string;
}
| {
successful: false;
// The OAuth 2.1 Error Code.
// Usually one of:
// ```
// invalid_request, unauthorized_client, access_denied, unsupported_response_type,
// invalid_scope, server_error, temporarily_unavailable
// ```
error: string;
// Human-readable ASCII text providing additional information, used to assist the
// developer in understanding the error that occurred.
error_description: string | null;
// A URI identifying a human-readable web page with information about the error,
// used to provide the client developer with additional information about the error.
error_uri: string | null;
};
export const parseOAuthCallbackParams = (location: string): CallbackParams => {
const params = new URLSearchParams(location);
const code = params.get("code");
if (code) {
return { successful: true, code };
}
const error = params.get("error");
const error_description = params.get("error_description");
const error_uri = params.get("error_uri");
if (error) {
return { successful: false, error, error_description, error_uri };
}
return {
successful: false,
error: "invalid_request",
error_description: "Missing code or error in response",
error_uri: null,
};
};
export const generateOAuthErrorDescription = (
params: Extract<CallbackParams, { successful: false }>,
): string => {
const error = params.error;
const errorDescription = params.error_description;
const errorUri = params.error_uri;
return [
`Error: ${error}.`,
errorDescription ? `Details: ${errorDescription}.` : "",
errorUri ? `More info: ${errorUri}.` : "",
]
.filter(Boolean)
.join("\n");
};