Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 56 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# react-oauth2-code-pkce

[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/soofstad/react-oauth2-pkce/blob/main/LICENSE) ![NPM Version](https://img.shields.io/npm/v/react-oauth2-code-pkce?logo=npm&label=version) ![NPM Downloads](https://img.shields.io/npm/d18m/react-oauth2-code-pkce?logo=npm) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-oauth2-code-pkce?label=size) ![CI](https://github.com/soofstad/react-oauth2-pkce/actions/workflows/tests.yaml/badge.svg)

React package for OAuth2 Authorization Code flow with PKCE

Adhering to the RFCs recommendations, cryptographically sound, and with __zero__ dependencies!
Adhering to the RFCs recommendations, cryptographically sound, and with __zero__ dependencies!

## What is OAuth2 Authorization Code Flow with Proof Key for Code Exchange?

Short version;
Short version;
The modern and secure way to do authentication for mobile and web applications!

Long version;
<https://www.rfc-editor.org/rfc/rfc6749.html>
<https://datatracker.ietf.org/doc/html/rfc7636>
Long version;
<https://www.rfc-editor.org/rfc/rfc6749.html>
<https://datatracker.ietf.org/doc/html/rfc7636>
<https://oauth.net/2/pkce/>
<https://datatracker.ietf.org/doc/html/rfc8252>

## Features

Expand All @@ -23,6 +25,7 @@ Long version;
- Session expired callback
- Silently refreshes short-lived access tokens in the background
- Decodes JWT's
- Native login flow for mobile devices
- A total of ~440 lines of code, easy for anyone to audit and understand

## Example
Expand Down Expand Up @@ -57,7 +60,7 @@ ReactDOM.render(<AuthProvider authConfig={authConfig}>
)
```

For more advanced examples, see `./examples/`.
For more advanced examples, see `./examples/`.

## Install

Expand All @@ -77,16 +80,20 @@ The object that's returned by `useContext(AuthContext)` provides these values;
interface IAuthContext {
// The access token. This is what you will use for authentication against protected Web API's
token: string
// An object with all the properties encoded in the token (username, email, etc.), if the token is a JWT
// An object with all the properties encoded in the token (username, email, etc.), if the token is a JWT
tokenData?: TTokenData
// Function to trigger login.
// Function to trigger login.
// If you want to use 'state', you might want to set 'clearURL' configuration parameter to 'false'.
// Note that most browsers block popups by default. The library will print a warning and fallback to redirect if the popup is blocked
logIn: (state?: string, additionalParameters?: { [key: string]: string | boolean | number }, method: TLoginMethod = 'redirect') => void
logIn: (state?: string, additionalParameters?: {
[key: string]: string | boolean | number
}, method: TLoginMethod = 'redirect') => void
// Function to trigger logout from authentication provider. You may provide optional 'state', and 'logout_hint' values.
// See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout for details.
logOut: (state?: string, logoutHint?: string, additionalParameters?: { [key: string]: string | boolean | number }) => void
// Keeps any errors that occured during login, token fetching/refreshing, decoding, etc..
logOut: (state?: string, logoutHint?: string, additionalParameters?: {
[key: string]: string | boolean | number
}) => void
// Keeps any errors that occured during login, token fetching/refreshing, decoding, etc..
error: string | null
// The idToken, if it was returned along with the access token
idToken?: string
Expand All @@ -100,9 +107,11 @@ interface IAuthContext {
### Configuration parameters

__react-oauth2-code-pkce__'s goal is to "just work" with any authentication provider that either
supports the [OAuth2](https://datatracker.ietf.org/doc/html/rfc7636) or [OpenID Connect](https://openid.net/developers/specs/) (OIDC) standards.
However, many authentication providers are not following these standards, or have extended them.
With this in mind, if you are experiencing any problems, a good place to start is to see if the provider expects some custom parameters.
supports the [OAuth2](https://datatracker.ietf.org/doc/html/rfc7636)
or [OpenID Connect](https://openid.net/developers/specs/) (OIDC) standards.
However, many authentication providers are not following these standards, or have extended them.
With this in mind, if you are experiencing any problems, a good place to start is to see if the provider expects some
custom parameters.
If they do, these can be injected into the different calls with these configuration options;

- `extraAuthParameters`
Expand Down Expand Up @@ -137,7 +146,11 @@ type TAuthConfig = {
postLogin?: () => void // default: () => null
// Which method to use for login. Can be 'redirect', 'replace', or 'popup'
// Note that most browsers block popups by default. The library will print a warning and fallback to redirect if the popup is blocked
loginMethod: 'redirect' | 'replace' | 'popup' // default: 'redirect'
loginMethod: 'redirect' | 'replace' | 'popup' | 'native' // default: 'redirect'
// Optional callback function for the native login method. If loginMethod is set to 'native', this function,
// which become required, will be called with the URL to open in the native browser(browser in app or somewhere else).
// This is used to open the native login flow on mobile devices.
onLoginUrlReady?: (url: string) => void
// Optional callback function for the 'refreshTokenExpired' event.
// You likely want to display a message saying the user need to log in again. A page refresh is enough.
onRefreshTokenExpire?: (event: TRefreshTokenExpiredEvent) => void // default: undefined
Expand All @@ -149,7 +162,7 @@ type TAuthConfig = {
autoLogin?: boolean // default: true
// Store login state in 'localStorage' or 'sessionStorage'
// If set to 'session', no login state is persisted by 'react-oauth2-code-pkce` when the browser closes.
// NOTE: Many authentication servers will keep the client logged in by cookies. You should therefore use
// NOTE: Many authentication servers will keep the client logged in by cookies. You should therefore use
// the logOut() function to properly log out the client. Or configure your server not to issue cookies.
storage?: 'local' | 'session' // default: 'local'
// Sets the prefix for keys used by this library in storage
Expand All @@ -175,7 +188,7 @@ type TAuthConfig = {
// Whether or not to post 'scope' when refreshing the access token
refreshWithScope?: boolean // default: true
// Controls whether browser credentials (cookies, TLS client certificates, or authentication headers containing a username and password) are sent when requesting tokens.
// Warning: Including browser credentials deviates from the standard protocol and can introduce unforeseen security issues. Only set this to 'include' if you know what
// Warning: Including browser credentials deviates from the standard protocol and can introduce unforeseen security issues. Only set this to 'include' if you know what
// you are doing and CSRF protection is present. Setting this to 'include' is required when the token endpoint requires client certificate authentication, but likely is
// not needed in any other case. Use with caution.
tokenRequestCredentials?: 'same-origin' | 'include' | 'omit' // default: 'same-origin'
Expand All @@ -187,16 +200,21 @@ type TAuthConfig = {

### Sessions expire too quickly

A session expire happens when the `refresh_token` is no longer valid and can't be used to fetch a new valid `access_token`.
A session expire happens when the `refresh_token` is no longer valid and can't be used to fetch a new valid
`access_token`.
This is governed by the `expires_in`, and `refresh_expires_in | refresh_token_expires_in`, in the token response.
If the response does not contain these values, the library assumes a quite conservative value.
You should configure your IDP (Identity Provider) to send these, but if that is not possible, you can set them explicitly
If the response does not contain these values, the library assumes a quite conservative value.
You should configure your IDP (Identity Provider) to send these, but if that is not possible, you can set them
explicitly
with the config parameters `tokenExpiresIn` and `refreshTokenExpiresIn`.

### Fails to compile with Next.js
The library's main componet `AuthProvider` is _client side only_. Meaning it must be rendered in a web browser, and can not be pre-rendered server-side (which is default in newer versions of NextJS and similar frameworks).

This can be solved by marking the module with `use client` and importing the component in the client only (`"ssr": false`).
The library's main componet `AuthProvider` is _client side only_. Meaning it must be rendered in a web browser, and can
not be pre-rendered server-side (which is default in newer versions of NextJS and similar frameworks).

This can be solved by marking the module with `use client` and importing the component in the client only (
`"ssr": false`).

```tsx
'use client'
Expand All @@ -222,25 +240,33 @@ export default function Authenticated() {
### Error `Bad authorization state...`

This is most likely to happen if the authentication at the identity provider got aborted in some way.
You might also see the error `Expected to find a '?code=' parameter in the URL by now. Did the authentication get aborted or interrupted?` in the console.
You might also see the error
`Expected to find a '?code=' parameter in the URL by now. Did the authentication get aborted or interrupted?` in the
console.

First of all, you should handle any errors the library throws. Usually, hinting at the user reload the page is enough.

Some known causes for this is that instead of logging in at the auth provider, the user "Registers" or "Reset password" or
something similar instead. Any such functions should be handled outside of this library, with separate buttons/links than the "Log in" button.
Some known causes for this is that instead of logging in at the auth provider, the user "Registers" or "Reset password"
or
something similar instead. Any such functions should be handled outside of this library, with separate buttons/links
than the "Log in" button.

### After redirect back from auth provider with `?code`, no token request is made

If you are using libraries that intercept any `fetch()`-requests made. For example `@tanstack/react-query`. That can cause
issues for the _AuthProviders_ token fetching. This can be solved by _not_ wrapping the `<AuthProvider>` in any such library.
If you are using libraries that intercept any `fetch()`-requests made. For example `@tanstack/react-query`. That can
cause
issues for the _AuthProviders_ token fetching. This can be solved by _not_ wrapping the `<AuthProvider>` in any such
library.

This could also happen if some routes in your app are not wrapped by the `<AuthProvider>`.

### The page randomly refreshes in the middle of a session

This will happen if you haven't provided a callback-function for the `onRefreshTokenExpire` config parameter, and the refresh token expires.
You probably want to implement some kind of "alert/message/banner", saying that the session has expired and that the user needs to log in again.
Either by refreshing the page, or clicking a "Log in" button.
This will happen if you haven't provided a callback-function for the `onRefreshTokenExpire` config parameter, and the
refresh token expires.
You probably want to implement some kind of "alert/message/banner", saying that the session has expired and that the
user needs to log in again.
Either by refreshing the page or clicking a "Log in" button.

## Develop

Expand Down
24 changes: 24 additions & 0 deletions examples/react-capacitor-app/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
69 changes: 69 additions & 0 deletions examples/react-capacitor-app/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...

// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,

// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
101 changes: 101 additions & 0 deletions examples/react-capacitor-app/app/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore

# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# Google Services (e.g. APIs or Firebase)
# google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/

# Android Profiling
*.hprof

# Cordova plugins for Capacitor
capacitor-cordova-android-plugins

# Copied web assets
app/src/main/assets/public

# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
2 changes: 2 additions & 0 deletions examples/react-capacitor-app/app/android/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
Loading
Loading