Coder Social home page Coder Social logo

jasonraimondi / ts-oauth2-server Goto Github PK

View Code? Open in Web Editor NEW
220.0 6.0 46.0 2.55 MB

A standards compliant implementation of an OAuth 2.0 authorization server for Nodejs that utilizes JWT and Proof Key for Code Exchange (PKCE), written in TypeScript.

Home Page: https://tsoauth2server.com/

License: MIT License

TypeScript 100.00%
oauth2-server node authorization-server pkce rfc6749 rfc6750 rfc7519 rfc7636 authorization-code-flow implicit-flow

ts-oauth2-server's Introduction

TypeScript OAuth2.0 Server

JSR GitHub package.json version GitHub Workflow Status Test Coverage NPM Downloads

@jmondi/oauth2-server is a standards compliant implementation of an OAuth 2.0 authorization server for Node, written in TypeScript.

Requires node >= 18

The following RFCs are implemented:

Out of the box it supports the following grants:

Any framework should work, here are example adapters for Express and Fastify.

Example implementations:

The included adapters are just helper functions, really any framework should be supported. Take a look at the adapter implementations for express and fastify to learn how you can implement one for your favorite tool!

Getting Started

Save some eye strain, use the documentation site

Install

pnpm add @jmondi/oauth2-server

We're now on JSR: The JavaScript Registry

npx jsr add @jmondi/oauth2-server

Security

Version Latest Version Security Updates
3.x πŸŽ‰ πŸŽ‰
2.x πŸŽ‰

Endpoints

The server uses two endpoints, GET /authorize and POST /token.

The Token Endpoint is a back channel endpoint that issues a use-able access token.

The Authorize Endpoint is a front channel endpoint that issues an authorization code. The authorization code can then be exchanged to the AuthorizationServer endpoint for a use-able access token.

The Token Endpoint

import {
 handleExpressResponse,
 handleExpressError,
} from "@jmondi/oauth2-server/express";

app.post("/token", async (req: Express.Request, res: Express.Response) => {
 const request = requestFromExpress(req);
 try {
  const oauthResponse = await authorizationServer.respondToAccessTokenRequest(request);
  return handleExpressResponse(res, oauthResponse);
 } catch (e) {
  handleExpressError(e, res);
  return;
 }
});

Authorize Endpoint

The /authorize endpoint is a front channel endpoint that issues an authorization code. The authorization code can then be exchanged to the AuthorizationServer endpoint for a useable access token.

The endpoint should redirect the user to login, and then to accept the scopes requested by the application, and only when the user accepts, should it send the user back to the clients redirect uri.

We are able to add in scope acceptance and 2FA into our authentication flow.

import { requestFromExpress } from "@jmondi/oauth2-server/express";

app.get("/authorize", async (req: Express.Request, res: Express.Response) => {
  const request = requestFromExpress(req);

  try {
    // Validate the HTTP request and return an AuthorizationRequest.
    const authRequest = await authorizationServer.validateAuthorizationRequest(request);

    // You will probably redirect the user to a login endpoint. 
    if (!req.user) {
      res.redirect("/login")
      return;
    }
    // After login, the user should be redirected back with user in the session.
    // You will need to manage the authorization query on the round trip.
    // The auth request object can be serialized and saved into a user's session.

    // Once the user has logged in set the user on the AuthorizationRequest
    authRequest.user = req.user;
    
    // Once the user has approved or denied the client update the status
    // (true = approved, false = denied)
    authRequest.isAuthorizationApproved = getIsAuthorizationApprovedFromSession();

    // If the user has not approved the client's authorization request, 
    // the user should be redirected to the approval screen.
    if (!authRequest.isAuthorizationApproved) {
      // This form will ask the user to approve the client and the scopes requested.
      // "Do you authorize Jason to: read contacts? write contacts?"
      res.redirect("/scopes")
      return;
    }

    // At this point the user has approved the client for authorization.
    // Any last authorization requests such as Two Factor Authentication (2FA) can happen here.


    // Redirect back to redirect_uri with `code` and `state` as url query params.
    const oauthResponse = await authorizationServer.completeAuthorizationRequest(authRequest);
    return handleExpressResponse(res, oauthResponse);
  } catch (e) {
    handleExpressError(e, res);
  }
});

Authorization Server

The AuthorizationServer depends on the repositories. By default, no grants are enabled; each grant is opt-in and must be enabled when creating the AuthorizationServer.

You can enable any grant types you would like to support.

const authorizationServer = new AuthorizationServer(
  clientRepository,
  accessTokenRepository,
  scopeRepository,
  new JwtService("secret-key"),
);

// Enable as many or as few grants as you'd like.
authorizationServer.enableGrantTypes(
  "client_credentials",
  "refresh_token",
);

// with custom token TTL
authorizationServer.enableGrantTypes(
  ["client_credentials", new DateInterval("1d")],
  ["refresh_token", new DateInterval("1d")],
);

Repositories

There are a few repositories you are going to need to implement in order to create an AuthorizationServer.

Auth Code Repository

Client Repository

Scope Repository

Token Repository

User Repository

Entities

And a few entities.

Auth Code Entity

Client Entity

Scope Entity

Token Entity

User Entity

Grants

Grants are different ways a client can obtain an access_token that will authorize it to use the resource server.

Which Grant?

Deciding which grant to use depends on the type of client the end user will be using.

+-------+
| Start |
+-------+
    V
    |
    
    |
+------------------------+              +-----------------------+
| Have a refresh token?  |>----Yes----->|  Refresh Token Grant  |
+------------------------+              +-----------------------+
    V
    |
    No
    |
+---------------------+                
|     Who is the      |                  +--------------------------+
| Access token owner? |>---A Machine---->| Client Credentials Grant |
+---------------------+                  +--------------------------+
    V
    |
    |
   A User
    |
    |
+----------------------+                
| What type of client? |   
+----------------------+     
    |
    |                                 +---------------------------+
    |>-----------Server App---------->| Auth Code Grant with PKCE |
    |                                 +---------------------------+
    |
    |                                 +---------------------------+
    |>-------Browser Based App------->| Auth Code Grant with PKCE |
    |                                 +---------------------------+
    |
    |                                 +---------------------------+
    |>-------Native Mobile App------->| Auth Code Grant with PKCE |
                                      +---------------------------+

Client Credentials Grant

Full Docs

When applications request an access token to access their own resources, not on behalf of a user.

Flow

The client sends a POST to the /token endpoint with the following body:

  • grant_type must be set to client_credentials
  • client_id is the client identifier you received when you first created the application
  • client_secret is the client secret
  • scope is a string with a space delimited list of requested scopes. The requested scopes must be valid for the client.

The authorization server will respond with the following response.

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and can be used to authenticate into the resource server
  • scope is a space delimited list of scopes the token has access to

Authorization Code Grant (w/ PKCE)

A temporary code that the client will exchange for an access token. The user authorizes the application, they are redirected back to the application with a temporary code in the URL. The application exchanges that code for the access token.

Flow

Part One

The client redirects the user to the /authorize with the following query parameters:

  • response_type must be set to code
  • client_id is the client identifier you received when you first created the application
  • redirect_uri indicates the URL to return the user to after authorization is complete, such as org.example.app://redirect
  • state is a random string generated by your application, which you’ll verify later
  • code_challenge must match the The code challenge as generated below,
  • code_challenge_method – Either plain or S256, depending on whether the challenge is the plain verifier string or the SHA256 hash of the string. If this parameter is omitted, the server will assume plain.

The user will be asked to login to the authorization server and approve the client and requested scopes.

If the user approves the client, they will be redirected from the authorization server to the provided redirect_uri with the following fields in the query string:

  • code is the authorization code that will soon be exchanged for a token
  • state is the random string provided and should be compared against the initially provided state
Part Two

The client sends a POST to the /token endpoint with the following body:

  • grant_type must be set to authorization_code
  • client_id is the client identifier you received when you first created the application
  • client_secret (optional) is the client secret and should only be provided if the client is confidential
  • redirect_uri
  • code_verifier
  • code is the authorization code from the query string

The authorization server will respond with the following response

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and is used to authenticate into the resource server
  • refresh_token is a JWT signed token and can be used in with the refresh grant
  • scope is a space delimited list of scopes the token has access to

Code Verifier

The code_verifier is part of the extended β€œPKCE” and helps mitigate the threat of having authorization codes intercepted.

Before initializing Part One of the authorization code flow, the client first creats a code_verifier. This is a cryptographically random string using the characters A-Z, a-z, 0-9, and the punctuation characters -._~ (hyphen, period, underscore, and tilde), between 43 and 128 characters long.

We can do this in Node using the native crypto package and a base64urlencode function:

import crypto from "node:crypto";

const code_verifier = crypto.randomBytes(43).toString("hex");

https://www.oauth.com/oauth2-servers/pkce/authorization-request/

Code Challenge

Now we need to create a code_challenge from our code_verifier.

For devices that can perform a SHA256 hash, the code challenge is a BASE64-URL-encoded string of the SHA256 hash of the code verifier.

const code_challenge = base64urlencode(
  crypto.createHash("sha256")
    .update(code_verifier)
    .digest()
);

Clients that do not have the ability to perform a SHA256 hash are permitted to use the plain code_verifier string as the code_challenge.

const code_challenge = code_verifier;

Refresh Token Grant

Access tokens eventually expire. The refresh token grant enables the client to obtain a new access_token from an existing refresh_token.

Flow

A complete refresh token request will include the following parameters:

  • grant_type must be set to refresh_token
  • client_id is the client identifier you received when you first created the application
  • client_secret if the client is confidential (has a secret), this must be provided
  • refresh_token must be the signed token previously issued to the client
  • scope (optional) the requested scope must not include any additional scopes that were not previously issued to the original token

The authorization server will respond with the following response

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and is used to authenticate into the resource server
  • refresh_token is a JWT signed token and can be used in with the refresh grant
  • scope is a space delimited list of scopes the token has access to

Password Grant

The Password Grant is for first party clients that are able to hold secrets (ie not Browser or Native Mobile Apps)

Flow

A complete refresh token request will include the following parameters:

  • grant_type must be set to password
  • client_id is the client identifier you received when you first created the application
  • client_secret if the client is confidential (has a secret), this must be provided
  • username
  • password
  • scope (optional)

The authorization server will respond with the following response

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and is used to authenticate into the resource server
  • refresh_token is a JWT signed token and can be used in with the refresh grant
  • scope is a space delimited list of scopes the token has access to

Implicit Grant

This grant is supported in the AuthorizationServer, but not recommended to use and thus is not documented. Industry best practice recommends using the Authorization Code Grant w/ PKCE for clients such as native and browser-based apps.

Please look at these great resources:

Revoke Token

Note: Implementing this endpoint is optional.

The /token/revoke endpoint is a back channel endpoint that revokes an existing token. Implementing this endpoint is optional.

app.post("/token/revoke", async (req: Express.Request, res: Express.Response) => {
  try {
    const oauthResponse = await authorizationServer.revoke(req);
    return handleExpressResponse(res, oauthResponse);
  } catch (e) {
    handleExpressError(e, res);
    return;
  }
});

Migration Guide

Thanks

This project is inspired by the PHP League's OAuth2 Server. Check out the PHP League's other packages for some other great PHP projects.

Star History

Star History Chart

ts-oauth2-server's People

Contributors

dependabot[bot] avatar filiptronicek avatar heldersi avatar jasonraimondi avatar leomercier avatar oliverlockwood avatar reiterbene avatar rssh avatar siddhant-k-code avatar thefat32 avatar xhebox 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

ts-oauth2-server's Issues

Error: secretOrPrivateKey must have a value

I have copied the example prisma_express from here:

https://github.com/jasonraimondi/ts-oauth2-server/tree/master/examples/prisma_express

I have then added a client and a user in my database and i'm trying to use postman to connect however i'm getting the following error:

Error: secretOrPrivateKey must have a value
    at Object.module.exports [as sign] (/auth-api/node_modules/jsonwebtoken/sign.js:107:20)
    at /auth-api/node_modules/@jmondi/oauth2-server/src/utils/jwt.ts:36:11
    at new Promise (<anonymous>)
    at JwtService.sign (/auth-api/node_modules/@jmondi/oauth2-server/src/utils/jwt.ts:35:12)
    at AuthCodeGrant.encrypt (/auth-api/node_modules/@jmondi/oauth2-server/src/grants/abstract/abstract.grant.ts:258:21)
    at AuthCodeGrant.completeAuthorizationRequest (/auth-api/node_modules/@jmondi/oauth2-server/src/grants/auth_code.grant.ts:213:29)
    at AuthorizationServer.completeAuthorizationRequest (/auth-api/node_modules/@jmondi/oauth2-server/src/authorization_server.ts:125:12)
[ERROR] 16:35:46 Error: secretOrPrivateKey must have a value

I have tried as a normal postman request and also using the OAuth function in authorization.

Here is the postman collection exported:

{
	"info": {
		"_postman_id": "ca96c9f8-5d76-43c8-beec-5b8aa6c76289",
		"name": "auth-api",
		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
	},
	"item": [
		{
			"name": "index",
			"request": {
				"method": "GET",
				"header": [],
				"url": {
					"raw": "{{base_url}}",
					"host": [
						"{{base_url}}"
					]
				}
			},
			"response": []
		},
		{
			"name": "authorize",
			"request": {
				"method": "GET",
				"header": [],
				"url": {
					"raw": "{{base_url}}/authorize?response_type=code&client_id=f9e3bd26-703a-4133-8eb2-05a2a5ae36d2&redirect_uri=http://localhost:4000&state=123456789&code_challenge=92d3b56942866d1edf02c33339b7c3dc37c6201282bb238cb47f0d3289f28a93f1bdd8af6ca9913aed0c4c&code_challenge_method=S256",
					"host": [
						"{{base_url}}"
					],
					"path": [
						"authorize"
					],
					"query": [
						{
							"key": "response_type",
							"value": "code"
						},
						{
							"key": "client_id",
							"value": "f9e3bd26-703a-4133-8eb2-05a2a5ae36d2"
						},
						{
							"key": "redirect_uri",
							"value": "http://localhost:4000"
						},
						{
							"key": "state",
							"value": "123456789"
						},
						{
							"key": "code_challenge",
							"value": "92d3b56942866d1edf02c33339b7c3dc37c6201282bb238cb47f0d3289f28a93f1bdd8af6ca9913aed0c4c"
						},
						{
							"key": "code_challenge_method",
							"value": "S256"
						}
					]
				}
			},
			"response": []
		},
		{
			"name": "authorize-auth-code",
			"request": {
				"auth": {
					"type": "oauth2",
					"oauth2": [
						{
							"key": "scope",
							"value": "code",
							"type": "string"
						},
						{
							"key": "accessTokenUrl",
							"value": "http://{{base_url}}/authorize",
							"type": "string"
						},
						{
							"key": "useBrowser",
							"value": false,
							"type": "boolean"
						},
						{
							"key": "grant_type",
							"value": "authorization_code_with_pkce",
							"type": "string"
						},
						{
							"key": "authUrl",
							"value": "http://{{base_url}}/authorize",
							"type": "string"
						},
						{
							"key": "redirect_uri",
							"value": "http://localhost:4000",
							"type": "string"
						},
						{
							"key": "clientSecret",
							"value": "123456",
							"type": "string"
						},
						{
							"key": "clientId",
							"value": "f9e3bd26-703a-4133-8eb2-05a2a5ae36d2",
							"type": "string"
						},
						{
							"key": "state",
							"value": "123456789",
							"type": "string"
						},
						{
							"key": "addTokenTo",
							"value": "header",
							"type": "string"
						}
					]
				},
				"method": "GET",
				"header": [],
				"url": {
					"raw": "{{base_url}}/authorize",
					"host": [
						"{{base_url}}"
					],
					"path": [
						"authorize"
					]
				}
			},
			"response": []
		}
	]
}

Module '"@jmondi/oauth2-server"' has no exported member 'OAuthException'.

Hi, I have the following code, but I can't find the module declration for "OAuthException", some can help? Thanks

import type { NextApiRequest, NextApiResponse } from 'next';
import type { ApiResponseError } from './types';
import { OAuthException } from '@jmondi/oauth2-server';

import nextConnect from 'next-connect';

import corsMiddleware from './api-middlewares/cors-middleware';

const apiHandler = <T>() =>
  nextConnect<NextApiRequest, NextApiResponse<ApiResponseError | T>>({
    onNoMatch: (_req, res) => res.status(405).json({ status: 405, message: 'Method Not Allowed' }),
    onError: (err, _req, res) => {
      if (err instanceof OAuthException) {
        return res.status(err.status).json({ status: err.status, message: err.error });
      }

      return res.status(500).json({ status: 500, message: 'Internal Server Error' });
    },
  }).use(corsMiddleware);

export default apiHandler;

Inconsistency: use of `isRevoked()` in the `refresh_token` grant is different from its use in the `auth_code` grant, and is unintuitive

In the refresh_token grant the code:

  1. first calls tokenRepository.getByRefreshToken() to get the whole OAuth access/refresh token object from the refresh token ID
  2. then calls tokenRepository.isTokenRevoked(), passing the full refresh token, to ask if the token has been revoked.

Conversely, the auth_code grant here and here

  1. first calls authCodeRepository.isRevoked(), passing the auth code ID, to ask if the token has been revoked
  2. later calls authCodeRepository.getByIdentifier(), again passing the auth code ID, to get the auth code object.

To my mind the approach currently taken is much more logical & intuitive, because e.g. if the token/code is revoked, you might not even be able to get it, depending on how consumer application implementations behave, so you end up duplicating logic in the getByRefreshToken() method that logically fit in the isTokenRevoked() method.

I am happy to raise a PR to tweak the refresh_token grant in this direction (in fact I already started coding a change before I stopped to raise this issue), but I thought I should first ask if there had been a reason that I haven't figured out which explains why the current approach is the way it is, and why such a change as I propose might not be appropriate?

Support for any port when redirectUri is a loopback URI

As per RFC 8252, when a native client acts as a temporary server in order to receive the OAuth code, it uses a loopback redirect URI. The port used for this shouldn't be hardcoded as it's possible it won't be available, and also there are security concerns if the port is fixed.

The RFC states:

The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request.

I'd be grateful if this server could support this, so we can specify redirectUris: ["http://127.0.0.1/oauth2callback"] in a client entry and it will work regardless of if the client sends that redirectUri containing any port number.

create-export script is not from package install

looks like
"pretest": "run-s create-exports". in package.local call something absent in package.json
(i.e. running on fresh node installation cause error)
ERROR: Task not found: "create-exports"

code_verifier digest with "hex" or not?

Hi, forks.

First, thanks for this project initiative. It is helping me a lot. However, I have faced some issues trying to use code_verifier and code_challenge. The (docs) say to digest code_verifier with "hex" before encoding to base64URL.

I did as docs said, but I always got a "Failed to verify code challenge". I went to the code and figured out that the verifyCodeChallenge() on S256Verifier doesn't use "hex" on disgest function. So, the code challenges always divert and fail verification, in order to solve it I remove "hex" digest from my code_challenge generator.

My question is: Which one is correct according to the RFCs, docs or the current code implementation?

PS: I'd love to help with a PR to fix it, just show me guidelines.

Thanks a lot.

OAuthException throw not supporting in latest version, gettting app crash when throw error from UserRepository

I'm not able to send throw in latest version "@jmondi/oauth2-server": "^3.0.2",
throw new OAuthException("The username or password is incorrect!!", ErrorType.InvalidRequest, undefined, undefined, HttpStatus.BAD_REQUEST);
when i'm throwing OAuthException or throw new Error("The username or password is incorrect!!") my app getting crash

         throw new OAuthException("The username or password is incorrect!!", ErrorType.InvalidRequest, undefined, undefined, HttpStatus.BAD_REQUEST);
              ^

OAuthException: The username or password is incorrect!!
at UserRepository. (C:\Users\Admin\Documents\InstinctIQ_AI\src\repository\UserRepository.ts:70:19)
at Generator.next ()
at fulfilled (C:\Users\Admin\Documents\InstinctIQ_AI\src\repository\UserRepository.ts:11:58) {
error: 'The username or password is incorrect!!',
errorType: 'invalid_request',
errorDescription: undefined,
errorUri: undefined,
status: 400
}

[nodemon] app crashed - waiting for file changes before starting...

but in previous version it is working as expected.
"@jmondi/oauth2-server": "^2.6.1",

please let me know that how to throw exeption that is user does not found or password not matched

Add configuration option to disallow the `plain` value for `code_challenge_method`?

Referring to https://www.rfc-editor.org/rfc/rfc7636#section-7.2:

"plain" SHOULD NOT be used and exists only for
compatibility with deployed implementations where the request path is
already protected. The "plain" method SHOULD NOT be used in new
implementations, unless they cannot support "S256" for some technical
reason.

I am working on a new OAuth2 server implementation, and I'd like to be able to be strict and disallow the plain method.
On this basis, I would propose adding an extra field in the AuthorizationServerOptions interface whereby:

  • it is named something like requiresS256
  • it has a default value of false
  • after this code in auth_code.grant.ts, we introduce another if test to enforce S256 as the codeChallengeMethod if the option is set.

Would you be willing to consider a change of this nature, if I were to raise a PR with associated unit tests?

Issues on the implementation

Hi, @jasonraimondi !

First of all, thanks for your good library :)

But I want to point out some problems while playing around it:

  1. client field in OAuthToken seems useless. I did not find any usage of this field, but it stores all the information(including secret) of client into the underlying storage of OAuthToken. It is also true for OAuthAuthCode, where the only usage seems to be .client.id. Similar cases happen to scopes and user field: only name field is used.
    I would suggest only storing the id for client and scopes fields. A secondary index of these ids is enough for users to extend the functionality.
  2. It is also strange that client.name is used as cid:
    Name is clearly not unique.
  3. Looks like it is not cleaned:
    console.log({ headers: this.headers, field });
    return "";
  4. I noticed that all we accept is RequestInterface, thus it can be things other than OAuthRequest. If I implement it on my own, then there is no normalization like ...toLowerCase(). I suggest adding that here:
    protected getRequestParameter(param: string, request: RequestInterface, defaultValue?: any) {
    return request.body?.[param] ?? defaultValue;
    }
    protected getQueryStringParameter(param: string, request: RequestInterface, defaultValue?: any) {
    return request.query?.[param] ?? defaultValue;
    }
  5. _response: ResponseInterface is another deprecated parameter, IMO. We can directly await authorizationServer.respondToAccessTokenRequest(req) without passing the response by responseFromExpress.
  6. Not async, but awaited:
    issueAuthCode(client: OAuthClient, user: OAuthUser | undefined, scopes: OAuthScope[]): OAuthAuthCode;

    const authCode = await this.authCodeRepository.issueAuthCode(client, user, scopes);

I am happy to issue PRs if you like these suggestions.

Suggestion: extend `OAuthClient` interface to (optionally) specify `accessTokenValidity` and `refreshTokenValidity` fields

In our implementation, we store the access token / refresh token validity (i.e. how long before each token type expires, after being issued) alongside our OAuth client configuration.

As such, and because the OAuthClient interface doesn't allow specifying this information (nor does it have any "custom" extensibility) we are unable to access this information at token generation time in our OAuthTokenRepository implementation, without having to inefficiently look up the client a second time in our underlying data source.

What I'd like to suggest is that:

  1. either we add optional fields accessTokenValidity and refreshTokenValidity to the OAuthClient interface, or we add custom extensibility via something like the [key: string]: any; field you have in the OAuthUser interface;
  2. additionally, regardless of which of the above options is chosen, we also extend the issueRefreshToken() call to provide the client as parameter 2 in all cases (I've checked, and all usages of this method have the client available to provide).

What do you think?

feat: allowed custom grant usage

What if we just allowed any arbitrary abstract grant to be initialized

diff --git a/src/authorization_server.ts b/src/authorization_server.ts
index 0a394b7..19e3d86 100644
--- a/src/authorization_server.ts
+++ b/src/authorization_server.ts
@@ -17,6 +17,7 @@ import { DateInterval } from "./utils/date_interval.js";
 import { JwtInterface, JwtService } from "./utils/jwt.js";
 import { DEFAULT_AUTHORIZATION_SERVER_OPTIONS } from "./options.js";
 import { ProcessTokenExchangeFn, TokenExchangeGrant } from "./grants/token_exchange.grant.js";
+import { AbstractGrant } from "./grants/abstract/abstract.grant.js";

 /**
  * @see https://tsoauth2server.com/configuration/
@@ -45,6 +46,9 @@ export type EnableableGrants =
   | {
       grant: "urn:ietf:params:oauth:grant-type:token-exchange";
       processTokenExchange: ProcessTokenExchangeFn;
+    }
+  | {
+      grant: AbstractGrant;
     };
 export type EnableGrant = EnableableGrants | [EnableableGrants, DateInterval];

@@ -135,6 +139,8 @@ export class AuthorizationServer {
         this.jwt,
         this.options,
       );
+    } else if (toEnable.grant instanceof AbstractGrant) {
+      grant = toEnable.grant;
     }

     if (!grant) {

Then you should be able to enable it like any other grant.

const authorizationServer = new AuthorizationServer(
  clientRepository,
  accessTokenRepository,
  scopeRepository,
  new JwtService("secret-key"),
);

const customGrant = new CustomOTPGrant(...);

authorizationServer.enableGrantTypes(
  ["client_credentials", new DateInterval("1d")],
  [customGrant, new DateInterval("1d")],
);

What are your thoughts on this idea?

Originally posted by @jasonraimondi in #127 (reply in thread)

Misleading error message `unsupported grant_type` when `grant_type` is specified and correct, but other required parameters are missing or incorrect

Context

First of all: thank you for developing this library and sharing it with the OSS community.

I'm getting stuck in to a PoC to integrate your library with my application, following the tutorial. In my initial setup I called:

authorizationServer.enableGrantType('authorization_code');

Observed behaviour

If I hit:

https://my-redacted-host/context/authorize

without any query parameters, I get a response like

{
  "status": 400,
  "message": "unsupported grant_type"
}

Which is fair enough. However, if I add query parameters one by one, like:

  1. grant_type=authorization_code
  2. response_type=code
  3. client_id=something

It is only when I have all 3 parameters that the message changes, in this case, giving a response:

{
  "status": 400,
  "message": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed: The authorization server requires public clients to use PKCE RFC-7636"
}

Which again, is fair enough (because I haven't provided code_challenge).

Expected behaviour

I would expect that with query params like ?grant_type=authorization_code, and ?grant_type=authorization_code&response_type=code, I should see a more useful error message; "unsupported grant_type" is rather misleading in this case, as I have already provided it correctly - it's other parameter(s) that are missing, and the error message should reflect this.

I look forward to your thoughts. Thanks again.

Client_credentials grant - client secrets validation

Hello, @jasonraimondi !

Firstly, I wanted to say a big thank you for the work you've put into this package. I really like how flexible it is and very well documented.

What I wanted to ask is why there is a second check comparing the provided and saved client secret when presumably we should have already made that check through the custom check in the ClientRepository.

To be more specific, I'm looking at lines 83 - 91 in abstract.grant.js. My understanding is that this.clientRepository.isClientValid(grantType, client, clientSecret); should contain our implementation where we compare the client secrets and allowed grant types. If we do the check there, I'm not sure why there is a need for a second validation... Can we remove it altogether?

The reason why I'm raising this as an issue is because I want to store hashes of the client secrets instead of the raw values, in which case the extra check (lines 87-91) results in an error.

Thank you for the time and consideration.

image

Trouble installing packages for example projects

I'm having trouble running the sample projects after installing packages using PNPM.

Error:
CleanShot 2023-02-15 at 00 37 42@2x

How to reproduce:

  1. Download this git repo
  2. In the root directory, run pnpm install
  3. cd examples/prisma_express
  4. Run pnpm run dev
  5. Observe error in the screenshot above

Error running Prisma/Express example - Client authentication failed: Invalid redirect_uri

I'm trying to run prisma_express example to generate an Authentication Code with PKCE.

When I run this request: http://localhost:3000/authorize?response_type=code&client_id=b1a639b9-09b6-427c-a6c5-55f10746949c&redirect_uri=http://example.com&state=abc123&code_challenge=hA3IxucyJC0BsZH9zdYvGeK0ck2dC-seLBn20l18Iws&code_challenge_method=S256, I receive {"status":401,"message":"Client authentication failed: Invalid redirect_uri"} in the browser.

The only entry I have in my Postgres DB is:
CleanShot 2023-02-15 at 01 01 18@2x

Here's a quick n dirty repo sample I'm using: https://github.com/bdcorps/ts-oauth2-server-example-problem

Is there something I'm missing here?

Cannot find module '@jmondi/oauth2-server/dist/adapters/fastify'

Typechecks work but when I try to start my fastify server it does not find the module.
After looking into the node_modules-Folder inside @jmondi/oauth2-server/dist/adapters I only find the typing files (*.d.ts) but no code. Also when searching inside the oauth2-server.cjs.development.js the function declaration can't be found.

Are there specific typescript options I have to set for the import to work?

Update: I also checked out the prisma-fastify-example and tried to start it up. Same error.

Sending an invalid `code` to the token endpoint results in an uncaught exception, instead of error response

Scenario

  1. Implement along the lines of the tutorial, i.e. https://jasonraimondi.github.io/ts-oauth2-server/getting_started/#the-authorization-server
app.post("/token", async (req: Express.Request, res: Express.Response) => {
  try {
    const oauthResponse = await authorizationServer.respondToAccessTokenRequest(req);
    return handleExpressResponse(res, oauthResponse);
  } catch (e) {
    handleExpressError(e, res);
    return;
  }
});
  1. Send in a token request with a code that isn't a valid JWT token, e.g.:
curl --location --request POST 'https://my-host-redacted/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=123' \
--data-urlencode 'redirect_uri=bishbash:/bosh' \
--data-urlencode 'code_verifier=somethingMadeUp' \
--data-urlencode 'code=asdasdasd'
  1. Observe an error in the logs, like jwt malformed with stack trace:
JsonWebTokenError: jwt malformed
    at Object.module.exports [as verify] (/opt/my-service/bin/node_modules/jsonwebtoken/verify.js:63:17)
    at null.<anonymous> (/opt/my-service/bin/node_modules/@jmondi/oauth2-server/src/utils/jwt.ts:17:11)
    at new Promise (<anonymous>)
    at JwtService.verify (/opt/my-service/bin/node_modules/@jmondi/oauth2-server/src/utils/jwt.ts:16:12)
    at AuthCodeGrant.decrypt (/opt/my-service/bin/node_modules/@jmondi/oauth2-server/src/grants/abstract/abstract.grant.ts:258:27)
    at AuthCodeGrant.respondToAccessTokenRequest (/opt/my-service/bin/node_modules/@jmondi/oauth2-server/src/grants/auth_code.grant.ts:49:38)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at OAuth2Service.token (/opt/my-service/bin/oauth2/oauth2-service.ts:174:35)

The curl itself never gets a response.

Also

The same sort of thing happens if you send in a JWT token with an unexpected signature, except the error is like JsonWebTokenError: invalid signature.

Proposed solution

Obviously one option would be to just note in the How To that consumers have to handle these cases themselves; however, instead I propose that the error handling functionality in handleExpressError() - and other implementations - be extended to handle JsonWebToken error instances also, and return 400 Bad Request like we do if (for example) the authorization code is valid but not found because it's been revoked or never existed.

refresh_token grant: Token is not linked to client when tokenCID set to 'name'.

Hi, and thank you all for such great library.

  1. I created an oauthServer with tokenCID equal to name
const oauthServer = new AuthorizationServer(clientRepository, accessTokenRepository, scopeRepository, jwtService, {
  // Client identifier `client.name` default to client.id
  tokenCID: 'name',
  // notBeforeLeeway
});
  1. The token generated contain cid as expected
{
  "iss": "65546b8f75de3edfd18eb8b3",
  // the cid set to my client name because of    tokenCID: 'name',
  "cid": "my-testing-app",
  "scope": "user.read",
  "sub": "65546b8f75de3edfd18eb8af",
  "exp": 1700117785,
  "nbf": 1700031385,
  "iat": 1700031385,
  "jti": "be2a66ba-7456-4bc2-baf5-10a6e6b4d188"
}
  1. I'm now testing the refresh_token grant and I got an error throwed by
if (refreshTokenData?.client_id !== clientId) {
      throw OAuthException.invalidParameter("refresh_token", "Token is not linked to client");
}
  1. Should We used the tokenCID to test against the clientId like you did when you generated the token ?
/// current code
const oldToken = await this.validateOldRefreshToken(req, client.id);
// should be like this or I'm missing somthing ?
const oldToken = await this.validateOldRefreshToken(req, client[this.options.tokenCID]);

Add custom claims on JWT access_token

Is there a way to add custom claims on the JWT access_token? maybe by implementing a custom JwtService or a middleware that we can call to add that extra claims.

Thanks.

Issue with typeorm example

It usage the Class name of entity rather than using actual table name
oauth_clients

`query: SELECT `OAuthClient`.`id` AS `OAuthClient_id`, `OAuthClient`.`secret` AS `OAuthClient_secret`, `OAuthClient`.`name` AS `OAuthClient_name`, `OAuthClient`.`redirectUris` AS `OAuthClient_redirectUris`, `OAuthClient`.`allowedGrants` AS `OAuthClient_allowedGrants`, `OAuthClient`.`createdAt` AS `OAuthClient_createdAt`, `OAuthClient__scopes`.`id` AS `OAuthClient__scopes_id`, `OAuthClient__scopes`.`name` AS `OAuthClient__scopes_name`, `OAuthClient__scopes`.`description` AS `OAuthClient__scopes_description` FROM `oauth_clients` `OAuthClient` LEFT JOIN `oauth_client_scopes` `OAuthClient_OAuthClient__scopes` ON `OAuthClient_OAuthClient__scopes`.`clientId`=`OAuthClient`.`id` LEFT JOIN `oauth_scopes` `OAuthClient__scopes` ON `OAuthClient__scopes`.`id`=`OAuthClient_OAuthClient__scopes`.`scopeId` WHERE `OAuthClient`.`id` IN (?) -- PARAMETERS: ["clientId"]
EntityNotFoundError: Could not find any entity of type "OAuthClient" matching: "clientId"`

Enhancement: support RFC 7009 for token revocation

RFC 7009 describes explicit revocation of OAuth2 tokens. For example, if a user running a session maintained by OAuth2 access & refresh tokens wishes to explicitly "log out" i.e. invalidate their current token set.

Of course, a consuming application could wire up their own logic directly to revoke tokens, but it occurred to me that it might be a nicety to support this as a function on the AuthorizationServer class.

ESBuild is unable to include package

I am unable to determine what is causing my build to fail now that I have required this package.

npm run build.src

const esb = require("esbuild")
esb.build({
  bundle: true,
  entryPoints: [
    "./src/index.ts",
  ],
  format: "esm",
  outdir: "./dist/",
  outExtension: {
    ".js": ".mjs",
  },
  platform: "neutral",
  sourcemap: true,
})
node build.src.js

> src/oauth2/server.ts:1:48: error: Could not resolve "@jmondi/oauth2-server" (mark it as external to exclude it from the bundle)
    1 Β¦ import { AuthorizationServer, JwtService } from "@jmondi/oauth2-server";
      ?                                                  ~~~~~~~~~~~~~~~~~~~~~

Build error Error: Build failed with 1 error:
src/oauth2/server.ts:1:48: error: Could not resolve "@jmondi/oauth2-server" (mark it as external to exclude it from the bundle)
    at failureErrorWithLog (D:\projects\project\node_modules\esbuild\lib\main.js:1475:15)
    at D:\projects\project\node_modules\esbuild\lib\main.js:1133:28
    at runOnEndCallbacks (D:\projects\project\node_modules\esbuild\lib\main.js:1051:65)
    at buildResponseToResult (D:\projects\project\node_modules\esbuild\lib\main.js:1131:7)
    at D:\projects\project\node_modules\esbuild\lib\main.js:1240:14
    at D:\projects\project\node_modules\esbuild\lib\main.js:611:9
    at handleIncomingPacket (D:\projects\project\node_modules\esbuild\lib\main.js:708:9)
    at Socket.readFromStdout (D:\projects\project\node_modules\esbuild\lib\main.js:578:7)
    at Socket.emit (node:events:390:28)
    at addChunk (node:internal/streams/readable:315:12)

allow to use regex to validate redirect uris

Right now it is only possible to whiltelist exact redirect URIs per a client. Would it be possible to use regex instead?

In our case we would like to prepend some query to redirect URI to transfer additional data. But it does not work since URIs don't match.

Feat Request: Allow Custom Parameters in Authorization Flow (e.g., Audience Parameter)

Hi, and thank you all for such a great library.

I'm using ts-oauth2-server as my OAuth 2.0 authorization server. and I have a use case where I need to pass a custom parameter (for me audience) in the authorization code flow.

Here are some references and examples of how other OAuth 2.0 implementations handle custom parameters, particularly audience:

  1. Ory: https://www.ory.sh/docs/hydra/guides/audiences#add-audiences-to-the-client-allow-list
  2. Auth0: https://community.auth0.com/t/why-is-it-necessary-to-pass-the-audience-parameter-to-receive-a-jwt/11412
  3. Postman allows to pass extra params like audience
Screenshot 2024-05-20 at 9 41 19β€―PM
  1. Keycloak: https://www.keycloak.org/docs/latest/securing_apps/#form-parameters
  2. And more like supertokens

Proposed Solution

  1. Allow custom queryparams.
  2. User Validation: Allow the user to validate these custom parameters.
  3. Ensure the custom query parameters are passed to theextraTokenFields function, allowing claims to be set based on these parameters (likeaudience).

Thanks!

What do you think about it?

Any tokens issued should be invalidated by reuse of the originating authorization_code

According to RFC 6749 section 4.1.2,

If an authorization code is used more than
once, the authorization server MUST deny the request and SHOULD
revoke (when possible) all tokens previously issued based on
that authorization code. The authorization code is bound to
the client identifier and redirection URI.

From my local testing, I believe that this library satisfies the MUST criterion, but not the SHOULD. It'd be nice to upgrade things.

I haven't yet given thought on how we might change the repository interfaces to achieve this - any suggestions, @jasonraimondi?

Refresh token - Old Token scope validation

During token refresh if in request there is no "scope" parameter library try to parse old token scopes

getRequestParameter accepts as parameter an array as strings while oldToken.scopes is {name: string}[]

i thing it can be fixed with a little change in RefreshTokenGrant#respondToAccessTokenRequest

const scopes = await this.validateScopes(this.getRequestParameter("scope", request, oldToken.scopes.map(s=>s.name)));

Add More Examples

I've just implemented typeorm entities + repositories in my project and would like to add them as examples

how do i implement consent flow?

It seems like consent or scopes approval flow is missing or is it up to developers?

Here's the logic code:

// Once the user has approved or denied the client update the status
    // (true = approved, false = denied)
    authRequest.isAuthorizationApproved = getIsAuthorizationApprovedFromSession();

    // If the user has not approved the client's authorization request, 
    // the user should be redirected to the approval screen.
    if (!authRequest.isAuthorizationApproved) {
      // This form will ask the user to approve the client and the scopes requested.
      // "Do you authorize Jason to: read contacts? write contacts?"
      req.redirect("/scopes")
      return;
    }

Do i need store myself what client and scopes are approved by user?

Thanks

Typescript compile error

node_modules/@jmondi/oauth2-server/src/grants/abstract/abstract_authorized.grant.ts:14:34 - error TS2345: Argument of type 'URLSearchParams | Record<string, string | string[]>' is not assignable to parameter of type 'string | URLSearchParams | string[][] | Record<string, string> | undefined'.
  Type 'Record<string, string | string[]>' is not assignable to type 'string | URLSearchParams | string[][] | Record<string, string> | undefined'.
    Type 'Record<string, string | string[]>' is missing the following properties from type 'string[][]': length, pop, push, concat, and 26 more.

14     params = new URLSearchParams(params);
                                    ~~~~~~


Found 1 error in node_modules/@jmondi/oauth2-server/src/grants/abstract/abstract_authorized.grant.ts:14

Support RFC 8693 - Token Exchange

I'm looking to implement Steam authentication into my app, alongside other strategies, and it's been deemed that the grant flow described in RFC 8693 is the appropriate way to do it. As far as I understand it, the flow would go like so:

  1. Steam game generates a Steam session ticket on the client's machine
  2. That ticket is sent to the auth server
  3. The auth server validates this ticket with Steam's web API
  4. If the ticket is valid, it passes an OAuth token back to the user that they can use to login, but never a refresh token, because this token should be tied to the user's Steam login session

The /token post request body seems to be as such:

client_id={client_id}
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
scope=some_scope
requested_token_type=urn:ietf:params:oauth:token-type:access_token
subject_token_type=urn:ietf:oauth:token-type:steam_session_ticket
subject_token={steam_session_ticket}

Where the second part (ietf here) is custom, and the last part of subject_token_type is custom.

Alternatively, is there a way I can create my own grant class which extends AbstractGrant and pass it to the AuthorizationServer instance?

Allow `userRepository.extraAccessTokenFields()` to set the `iss` (issuer) claim

At the moment, the implementation of encryptAccessToken() always sets the issuer claim (iss) to undefined - see https://github.com/jasonraimondi/ts-oauth2-server/blob/main/src/grants/abstract/abstract.grant.ts#L121.

I appreciate the link to the RFC documentation saying that use of this claim is OPTIONAL, however if it's optional, then surely consumers of the library should be allowed to set it πŸ˜ƒ

If you're happy with this proposal then I will put together a PR to make this change.

Question about credential flow

hey, jasonraimondi, thanks for your awesome project,
I wonder if there are multiple microservices distributed on different machines, and I have to protect those endpoints by access_token, then what is the best practice (or common implementation) to do so?

for example, i have microservices A, B, C
now, service A have to send request to protected service B (with access_token)

i am not sure how can i complete this workflow 😣

my current thought is use client_credential to grant all services, each service will verify token before send request to another service endpoint, if the token expired or not existed, then create it at first, after we get access token from this authorization server, send request with access token and target service will verify this access token before data processing.

would appreciate for any advice πŸ™πŸ™πŸ™

RFC7636 is not correctly implemented

Hi.
When I try to send s256 for code_challenge_method, I get the error:
The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed: Must be s256 or plain
which is incorrect according to specs: https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 (must be with the uppercase S - S256)

I tested it with Postman OAuth 2.0 Authorization type, so there is no way to change to s256 ..
image

Authorization code grant: wrong code in redirect URL

The problem

Hello, I am using this library to implement an OAuth authorization_code flow (sort of) for my containerized application.
I stumbled upon an error stealing two days of my time at least, when calling the /token endpoint.

My authorization server is continuously throwing this error:
The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed: Authorization code is expired or revoked

Debugging and explanation

As the authcodes shouldn't expire in less than 100 ms (the authcode TTL is 15m as the code states), I dug into the code and discovered a strange behavior in AuthCodeGrant.completeAuthorizationRequest(), which is the function being called when the /authorize endpoint - as you mention in the example repository https://github.com/jasonraimondi/ts-oauth2-server-example - ends its work.

Look at this code from line 214 of auth_code.grant.ts:

...
    const authCode = await this.issueAuthCode(
      this.authCodeTTL,
      authorizationRequest.client,
      authorizationRequest.user.id,
      authorizationRequest.redirectUri,
      authorizationRequest.codeChallenge,
      authorizationRequest.codeChallengeMethod,
      authorizationRequest.scopes,
    );

    const payload: IAuthCodePayload = {
      client_id: authCode.client.id,
      redirect_uri: authCode.redirectUri,
      auth_code_id: authCode.code,
      scopes: authCode.scopes.map(scope => scope.name),
      user_id: authCode.user?.id,
      expire_time: this.authCodeTTL.getEndTimeSeconds(),
      code_challenge: authorizationRequest.codeChallenge,
      code_challenge_method: authorizationRequest.codeChallengeMethod,
    };

    const jsonPayload = JSON.stringify(payload);

    const code = await this.encrypt(jsonPayload);

    const params: Record<string, string> = { code };
...

When the this.issueAuthCode function is called (shown below), the generated authcode is then persisted depending on the implementation of your authcode repository:

    const user = userIdentifier ? await this.userRepository.getUserByCredentials(userIdentifier) : undefined;

    const authCode = await this.authCodeRepository.issueAuthCode(client, user, scopes);
    authCode.expiresAt = authCodeTTL.getEndDate();
    authCode.redirectUri = redirectUri;
    authCode.codeChallenge = codeChallenge;
    authCode.codeChallengeMethod = codeChallengeMethod;
    authCode.scopes = [];
    scopes.forEach(scope => authCode.scopes.push(scope));
    await this.authCodeRepository.persist(authCode);
    return authCode;

but in the completeAuthorizationRequest() function the encrypted "jsonPayload" object is sent as a code in the redirect URL instead of the code into the authcode entity issued earlier. This results in the error Authorization code is expired or revoked, as I first search the authcode into my database by code (I am using Redis as storage) in order to check if the authcode is expired/revoked; since the persisted code is different than the one given in the redirect URL, I cannot find the associated authcode in the database and then cannot check if it was revoked or expired, thus generating this ambiguous error.

Expected behavior

The code sent in the redirect URL should be equal to the code of the previously issued authcode through this.issueAuthCode.

Is this intended behavior or am I completely misunderstanding the flow?

Thank you in advance.

Content-type error

Hello!

I'm using right now ts-oauth2-server to create a server that connects with Gitlab. After completeAuthorizationRequest is completed, handleExpressResponse return the response redirecting to the Gitlab callback, Gitlab responds that content-type is not supported.

content-type

It's possible to see the content-type is set to 'html/txt'. Looking in the OAuth2 RFC, apparently the response for the authentication code endpoint must have the content-type set to 'application/x-www-form-urlencoded'. I also take a look in the same response from others oauth2-server, like cognito, and seems like they don't have the same content-type set on header.

cognito

Maybe Gitlab is expecting to have a callback request with content-type set to 'application/x-www-form-urlencoded'. Let me know if understand something wrong about this problem, or if we have some type of inconsistency between the library and RFC or Gitlab.

aud

Why is the "aud" claim being set from extraAccessTokenFields in the JWT payload? I can not directly set this claim to a specific client ID, and furthermore, this method is not even utilized in the client credentials grant.

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.