Coder Social home page Coder Social logo

panva / oauth4webapi Goto Github PK

View Code? Open in Web Editor NEW
500.0 11.0 45.0 2.09 MB

OAuth 2 / OpenID Connect for JavaScript Runtimes

License: MIT License

TypeScript 96.47% JavaScript 1.86% Shell 1.67%
oauth2 openid openid-connect oidc authentication authorization browser cloudflare-workers deno electron

oauth4webapi's Introduction

OAuth 2 / OpenID Connect for JavaScript Runtimes

This software provides a collection of routines that can be used to build client modules for OAuth 2.1, OAuth 2.0 with the latest Security Best Current Practices (BCP), and FAPI 2.0, as well as OpenID Connect where applicable. The primary goal of this software is to promote secure and up-to-date best practices while using only the capabilities common to both browser and non-browser JavaScript runtimes.

Features

The following features are currently in scope and implemented in this software:

  • Authorization Server Metadata discovery
  • Authorization Code Flow (profiled under OpenID Connect 1.0, OAuth 2.0, OAuth 2.1, and FAPI 2.0), with PKCE
  • Refresh Token, Device Authorization, and Client Credentials Grants
  • Demonstrating Proof-of-Possession at the Application Layer (DPoP)
  • Token Introspection and Revocation
  • Pushed Authorization Requests (PAR)
  • UserInfo and Protected Resource Requests
  • Authorization Server Issuer Identification
  • JWT Secured Introspection, Response Mode (JARM), Authorization Request (JAR), and UserInfo
  • Validating incoming JWT Access Tokens

OpenID Certification

Filip Skokan has certified that this software conforms to the Basic, FAPI 1.0 Advanced, FAPI 2.0 Security Profile, and FAPI 2.0 Message Signing Relying Party Conformance Profiles of the OpenID Connectβ„’ protocol.

Dependencies: 0

oauth4webapi has no dependencies and it exports tree-shakeable ESM.

oauth4webapi is distributed via npmjs.com, deno.land/x, cdnjs.com, jsdelivr.com, and github.com.

example ESM import

import * as oauth2 from 'oauth4webapi'

example Deno import

import * as oauth2 from 'https://deno.land/x/[email protected]/mod.ts'
  • Authorization Code Flow (OAuth 2.0) - source
  • Authorization Code Flow (OpenID Connect) - source | diff
  • Extensions
  • Client Authentication
    • Client Secret in HTTP Authorization Header - source
    • Client Secret in HTTP Body - source | diff
    • Private Key JWT Client Authentication - source | diff
    • Public Client - source | diff
  • Other Grants
  • FAPI
    • FAPI 1.0 Advanced (Private Key JWT, MTLS, JAR) - source | diff
    • FAPI 2.0 Security Profile (Private Key JWT, PAR, DPoP) - source | diff

Supported Runtimes

The supported JavaScript runtimes include those that support the utilized Web API globals and standard built-in objects. These are (but are not limited to):

Out of scope

The following features are currently out of scope:

  • CommonJS
  • Implicit, Hybrid, and Resource Owner Password Credentials Flows
  • JSON Web Encryption (JWE)
  • Automatic polyfills of any kind

oauth4webapi's People

Contributors

blakeembrey avatar dependabot[bot] avatar github-actions[bot] avatar panva avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

oauth4webapi's Issues

'expires_in' response from Oauth token server is sometimes string

What happened?

I was using @auth/sveltekit with Azure-AD provider and ran into this issue. The expires_in is returned as a string, which fails this library's JWT checks.

Oauth Provider: Azure Active Directory (Azure AD)
Project: Sveltekit
Auth Library: @auth
Nodejs 18x

Version

2.3.0

Runtime

Node.js

Runtime Details

18.x

Code to reproduce

import { SvelteKitAuth } from '@auth/sveltekit'
import AzureAD, { type AzureADProfile } from '@auth/core/providers/azure-ad'
import type {
	AuthorizationEndpointHandler,
	OAuth2Config,
	TokenEndpointHandler
} from '@auth/core/providers/oauth'
const token: TokenEndpointHandler = {
	url: //url,
	request: async (context: any) => {
		console.log({ context })
	}
}
const authorization: AuthorizationEndpointHandler = {
	url: //url,
	request: async (context: any) => {
		console.log({ context })
	}
}
const params: OAuth2Config<AzureADProfile> = {
	id: 'azure',
	type: 'oauth',
	issuer: // issuer,
	name: 'Azure AD',
	authorization,
	token,
	clientId: process.env.AZURE_AD_CLIENT_ID,
	clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
	client: {
		client_id: process.env.AZURE_AD_CLIENT_ID,
		client_secret: process.env.AZURE_AD_CLIENT_SECRET,
		redirect_uris: ['https://localhost:5173/auth/login/azure'],
		default_max_age: 60 * 60,
		require_auth_time: true
	}
}

export const handle = SvelteKitAuth({
	providers: [
		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
		// @ts-ignore
		AzureAD({ ...params, tenantId: process.env.AZURE_AD_TENANT_ID })
	],
	secret: process.env.AUTH_SECRET,
	session: {
		maxAge: 60 * 60 // 1 hour
	}
})

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Allow custom discovery location?

What happened?

First of all, really awesome work making oauth4webapi and node-openid-client.

I'm not sure whether to label this a feature request or bug.

I'm running into an issue when sending a discovery request to the url:
https://developer.api.intuit.com/.well-known/openid_sandbox_configuration

You'll notice that oauth4webapi is appending its own ...well-known... url portion, so it appears in duplicate:

1B7F7D97-2497-4094-9DBC-7E99101FA4A3

It looks like you may have solved this in node-openid-client in this commit: panva/node-openid-client@0c8a8f2

I realize that is a different repo, but these projects do solve a lot of the same problems and I was wondering if you'd be willing to apply the same/similar fix to this repo?

Thanks,
Mike

Version

v2.2.1

Runtime

Deno

Runtime Details

deno:v1.32.4

Code to reproduce

import { discoveryRequest } from 'https://deno.land/x/[email protected]/mod.ts'


const url = new URL('https://developer.api.intuit.com/.well-known/openid_sandbox_configuration')

const response = await discoveryRequest(url)

console.log(response)


### Required

- [X] I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
- [X] I agree to follow this project's [Code of Conduct](https://github.com/panva/oauth4webapi/blob/main/CODE_OF_CONDUCT.md)

'aud' claim is optional according to RFC7519 but is required in this library

What happened?

AWS Cognito access tokens don't contain 'aud' claims which seems to be compliant with the RFC7519 spec but this library errors if it is not present. From the validateJwtAccessToken() function we have the following logic:

    const requiredClaims = [
        'iss',
        'exp',
        'aud',
        'sub',
        'iat',
        'jti',
        'client_id',
    ];
    if (options?.requireDPoP || scheme === 'dpop' || request.headers.has('dpop')) {
        requiredClaims.push('cnf');
    }
    const { claims } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, undefined, SUPPORTED_JWS_ALGS), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(options), getClockTolerance(options))
        .then(checkJwtType.bind(undefined, 'at+jwt'))
        .then(validatePresence.bind(undefined, requiredClaims))
        .then(validateIssuer.bind(undefined, as.issuer))
        .then(validateAudience.bind(undefined, expectedAudience));

Version

v2.10.3

Runtime

Node.js

Runtime Details

v20.11.1

Code to reproduce

{}

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Request: credentials property

What happened?

The library currently lacks support for including credentials such as cookies with requests made through the API. This feature is critical for interacting with endpoints that require the use of credentials for various purposes such as extra measures for authentication.

https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

Version

v2.3.0

Runtime

Browser

Runtime Details

18.18.0

Code to reproduce

Current Behavior:

When using the library to make requests, there is no option available to include credentials.
This results in failed requests when interacting with servers requiring credentials.
Expected Behavior:

The library should provide an option allowing developers to include credentials with requests.
This option should support different credential inclusion types such as 'include', 'same-origin', and 'omit'.
Steps to Reproduce:

Attempt to make a request to an endpoint that requires credentials.
Observe that there is no option to include credentials, and the request fails due to the absence of credentials.

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Your worker called response.clone(), but did not read the body of both clones

What happened?

Running this

const issuer = new URL("...");
const as = await oauth2
  .discoveryRequest(issuer)
  .then((response) => oauth2.processDiscoveryResponse(issuer, response))

causes Cloudflare Workers to issue the following warning:

Your worker called response.clone(), but did not read the body of both clones. This is wasteful, as it forces the system to buffer the entire response body in memory, rather than streaming it through. This may cause your worker to be unexpectedly terminated for going over the memory limit. If you only meant to copy the response headers and metadata (e.g. in order to be able to modify them), use new Response(response.body, response) instead.

Version

v2.0.1

Runtime

Cloudflare Workers

Runtime Details

Wrangler compatibility_date = "2022-11-25"

Code to reproduce

export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext
	): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname === "/") {
			const issuer = new URL("...");
			const as = await oauth2
				.discoveryRequest(issuer)
				.then((response) => oauth2.processDiscoveryResponse(issuer, response))

			console.log(as);

			return new Response(null, { headers, status: 302 });
		} else {
			return new Response(null, { status: 404 });
		}
	},
};

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

GitHub OAuth responds with 200 on error

What happened?

The line here assumes the error of the response has been handled, but the GitHub authorization response returns a 200 with the error property set in JSON. I believe the correct behavior would be to check if error has been set before checking for access_token and return early.

Version

1.0.4

Runtime

Cloudflare Workers

Runtime Details

N/A

Code to reproduce

  const response = await oauth2.authorizationCodeGrantRequest(
    as,
    client,
    parameters,
    getRedirectUri(),
    oauth2.generateRandomCodeVerifier()
  );

  const result = await oauth2.processAuthorizationCodeOAuth2Response(
    as,
    client,
    response
  );

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

nbf > now()

What happened?

In client environments with unsynced clocks (many of your users' computers when you scale), nbf timestamps can be ahead or behind the user's clock by an amount greater than the tolerance (tolerance itself is not really supposed to account for this inaccuracy and is more designed for network latency.)

What we see most often is users will have their current time set according to their wall clock, but their timezone is set to somewhere else in the world, so the coordinated time on their computer is off by several hours.

To overcome this, the most appropriate approach would be to allow developers to provide a custom async function to override the epochTime function. That way, we could for example make a call to our time server.

Version

latest

Runtime

Browser

Runtime Details

Any, incorrect timezone

Code to reproduce

Any code that validates the nbf will fail if the timezone is set incorrectly.

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Cross Origin Request Blocked: User Agent on discovery fetch

What happened?

I'm getting CORS errors with discoveryRequest and GitLab. Some minimal reproducable code:

const issuer = new URL("https://gitlab.com/.well-known/openid-configuration");
const as = await oauth.discoveryRequest(issuer);

I'm not a web developer, but I'm quite sure it is related to the library overriding the user-agent in prepareHeaders. Some minimal reproducable code without using the library:

const issuer = new URL("https://gitlab.com/.well-known/openid-configuration");

const headers = new Headers();
headers.set("user-agent", "oauth4webapi/v1.0.3"); // Works without this line
headers.set("accept", "application/json");

fetch(issuer.href, { headers, method: "GET", redirect: "manual" })
  .then((response) => response.json())
  .then((data) => console.log(data));

Should the user-agent really be the library? Usually it is the browser, isn't it?

Version

v1.0.3

Runtime

Browser

Runtime Details

Linux, Firefox & Chrome

Code to reproduce

const issuer = new URL("https://gitlab.com/.well-known/openid-configuration");
const as = await oauth.discoveryRequest(issuer);

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Issues with @remix-run/node runtime

What happened?

Using a more browser compliant fetch api like @remix-run/node uses will cause a lot of "response" must be an instance of Response errors due.

Similar to remix-run/react-router#9690

Version

2.0.6

Runtime

Node.js

Runtime Details

Node and Remix framework

Code to reproduce

import * as o from "oauth4webapi"
    
let discoveryResponse = await o.discoveryRequest(issuer)
const as = await o.processDiscoveryResponse(issuer, discoveryResponse)

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

"typ" header parameter value is optional according to RFC7519 but is required in this library

What happened?

AWS Cognito access tokens don't have the "typ" header parameter value which seems to be compliant with the RFC7519 spec but this library errors if it is not present. From the validateJwtAccessToken() function we have the following logic:

function checkJwtType(expected, result) {
    if (typeof result.header.typ !== 'string' || normalizeTyp(result.header.typ) !== expected) {
        throw new OPE('unexpected JWT "typ" header parameter value');
    }
    return result;
}

Version

v2.10.3

Runtime

Node.js

Runtime Details

v20.11.1

Code to reproduce

{}

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Auth code + PKCE without nonce check fails with an empty string for nonce in JWT

What happened?

When not including nonce as a check for oauth2 auth code + pkce, Hydra returns a JWT with an empty nonce field in it, but oauth4webapi seems to assume that it has to not be present at all.

image

if (claims.nonce !== undefined) {

Version

v2.2.1

Runtime

Node.js

Runtime Details

Node v16.15.0

Code to reproduce

No specific code available, tried to auth using @auth/core + @auth/sveltekit

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Node.js support

Fetch API and Web Crypto API globals are not available in prior Node.js versions but they can be enabled via node's CLI.

  • Node.js 18.x versions require the --experimental-global-webcrypto command-line flag.
  • Node.js 19.x and newer versions require no command-line flags πŸŽ‰.

These may be provided to the node executable as command-line flags

node --experimental-global-webcrypto ...

or via the NODE_OPTIONS environment variables, e.g.

export NODE_OPTIONS='--experimental-global-webcrypto'

Microsoft OpenID authorization URL acquisition does not expect proper URLs

What happened?

When using with auth/core 0.18.6 and with the Azure AD , I get this error showing up:

[auth][error] OperationProcessingError: "response" body "issuer" does not match "expectedIssuer"
logger.js:9
    at Module.processDiscoveryResponse (file:///path/to/project/node_modules/.pnpm/[email protected]/node_modules/oauth4webapi/build/index.js:252:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async getAuthorizationUrl (file:///path/to/project/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/lib/actions/signin/authorization-url.js:19:20)
    at async Module.signIn (file:///path/to/project/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/lib/actions/signin/index.js:10:56)
    at async AuthInternal (file:///path/to/project/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/lib/index.js:58:24)
    at async Proxy.Auth (file:///path/to/project/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/index.js:104:29)
    at async Module.respond (/path/to/project/node_modules/.pnpm/@[email protected][email protected][email protected]/node_modules/@sveltejs/kit/src/runtime/server/respond.js:282:20)
    at async file:///path/to/project/node_modules/.pnpm/@[email protected][email protected][email protected]/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:510:22

It gets the authentication URL from the correct place: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration', but when we process the result, we end up in this error.

In this check:

I used these hacks and it seems to work from what I can tell:

processDiscoveryResponse near the bottom:
    // if (new URL(json.issuer).href !== expectedIssuerIdentifier.href) {
    //     throw new OPE('"response" body "issuer" does not match "expectedIssuer"');
    // }
    json.issuer = expectedIssuerIdentifier.href;
    return json;

for some reason, validateIssuer gets supplied with a URL from some weird tenant ID and
it's freaking out because it isn't common. replace with:

function validateIssuer(expected, result) {
    // if (result.claims.iss !== expected) {
    //     throw new OPE('unexpected JWT "iss" (issuer) claim value');
    // }
    return result;
}

Let me know if this is an auth js core issue, but the nature in which azure expects tenant IDs to be replaced by the server/client makes this issue out to be like it's in this library's domain.

Version

2.6.0

Runtime

Node.js

Runtime Details

cba, honestly don't think it matters

Code to reproduce

/* this is just copy pasted from auth js core */
import * as checks from "../callback/oauth/checks.js";
import * as o from "oauth4webapi";
/**
 * Generates an authorization/request token URL.
 *
 * [OAuth 2](https://www.oauth.com/oauth2-servers/authorization/the-authorization-request/)
 */
export async function getAuthorizationUrl(query, options) {
    const { logger, provider } = options;
    let url = provider.authorization?.url;
    let as;
    // Falls back to authjs.dev if the user only passed params
    if (!url || url.host === "authjs.dev") {
        // If url is undefined, we assume that issuer is always defined
        // We check this in assert.ts
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const issuer = new URL(provider.issuer);
        const discoveryResponse = await o.discoveryRequest(issuer);
        const as = await o.processDiscoveryResponse(issuer, discoveryResponse); // HERE IS THE CAUSE OF THE ISSUE
        if (!as.authorization_endpoint) {
            throw new TypeError("Authorization server did not provide an authorization endpoint.");
        }
        url = new URL(as.authorization_endpoint);
    }
    const authParams = url.searchParams;
    let redirect_uri = provider.callbackUrl;
    let data;
    if (!options.isOnRedirectProxy && provider.redirectProxyUrl) {
        redirect_uri = provider.redirectProxyUrl;
        data = { origin: provider.callbackUrl };
        logger.debug("using redirect proxy", { redirect_uri, data });
    }
    const params = Object.assign({
        response_type: "code",
        // clientId can technically be undefined, should we check this in assert.ts or rely on the Authorization Server to do it?
        client_id: provider.clientId,
        redirect_uri,
        // @ts-expect-error TODO:
        ...provider.authorization?.params,
    }, Object.fromEntries(provider.authorization?.url.searchParams ?? []), query);
    for (const k in params)
        authParams.set(k, params[k]);
    const cookies = [];
    const state = await checks.state.create(options, data);
    if (state) {
        authParams.set("state", state.value);
        cookies.push(state.cookie);
    }
    if (provider.checks?.includes("pkce")) {
        if (as && !as.code_challenge_methods_supported?.includes("S256")) {
            // We assume S256 PKCE support, if the server does not advertise that,
            // a random `nonce` must be used for CSRF protection.
            if (provider.type === "oidc")
                provider.checks = ["nonce"];
        }
        else {
            const { value, cookie } = await checks.pkce.create(options);
            authParams.set("code_challenge", value);
            authParams.set("code_challenge_method", "S256");
            cookies.push(cookie);
        }
    }
    const nonce = await checks.nonce.create(options);
    if (nonce) {
        authParams.set("nonce", nonce.value);
        cookies.push(nonce.cookie);
    }
    // TODO: This does not work in normalizeOAuth because authorization endpoint can come from discovery
    // Need to make normalizeOAuth async
    if (provider.type === "oidc" && !url.searchParams.has("scope")) {
        url.searchParams.set("scope", "openid profile email");
    }
    logger.debug("authorization url is ready", { url, cookies, provider });
    return { redirect: url.toString(), cookies };
}

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

`client_secret_basic` incorrect encoding for special characters?

What happened?

The OAuth application I'm using has / and + values in the client secret.

This ends up resulting in my Authorization: Basic token_here being invalid.

Is this a bug in oauth4webapi, or the OAuth provider? (I'm new to OAuth, so not sure which needs to be fixed. =P

Version

v2.3.0

Runtime

Cloudflare Workers

Runtime Details

N/A Cloudflare Worker

Code to reproduce

// Sorry, heading to the airport at the moment, I can make a reproduction case later though if needed.

Required

  • I have searched the issues tracker and discussions for similar topics and couldn't find anything related.
  • I agree to follow this project's Code of Conduct

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.