134 |
135 |
136 |
137 |
138 | )
139 |
--------------------------------------------------------------------------------
/src/authentication.ts:
--------------------------------------------------------------------------------
1 | import { postWithXForm } from './httpUtils'
2 | import { generateCodeChallenge, generateRandomString } from './pkceUtils'
3 | import { calculatePopupPosition } from './popupUtils'
4 | import type {
5 | TInternalConfig,
6 | TLoginMethod,
7 | TPrimitiveRecord,
8 | TTokenRequest,
9 | TTokenRequestForRefresh,
10 | TTokenRequestWithCodeAndVerifier,
11 | TTokenResponse,
12 | } from './types'
13 |
14 | const codeVerifierStorageKey = 'PKCE_code_verifier'
15 | const stateStorageKey = 'ROCP_auth_state'
16 |
17 | export async function redirectToLogin(
18 | config: TInternalConfig,
19 | customState?: string,
20 | additionalParameters?: TPrimitiveRecord,
21 | method: TLoginMethod = 'redirect'
22 | ): Promise {
23 | const storage = config.storage === 'session' ? sessionStorage : localStorage
24 | const navigationMethod = method === 'replace' ? 'replace' : 'assign'
25 |
26 | // Create and store a random string in storage, used as the 'code_verifier'
27 | const codeVerifier = generateRandomString(96)
28 | // Prefix the code verifier key name to prevent multi-application collisions
29 | const codeVerifierStorageKeyName = config.storageKeyPrefix + codeVerifierStorageKey
30 | storage.setItem(codeVerifierStorageKeyName, codeVerifier)
31 |
32 | // Hash and Base64URL encode the code_verifier, used as the 'code_challenge'
33 | return generateCodeChallenge(codeVerifier).then((codeChallenge) => {
34 | // Set query parameters and redirect user to OAuth2 authentication endpoint
35 | const params = new URLSearchParams({
36 | response_type: 'code',
37 | client_id: config.clientId,
38 | code_challenge: codeChallenge,
39 | code_challenge_method: 'S256',
40 | ...config.extraAuthParameters,
41 | ...additionalParameters,
42 | })
43 |
44 | if (config.scope !== undefined && !params.has('scope')) {
45 | params.append('scope', config.scope)
46 | }
47 | if (config.redirectUri !== undefined && !params.has('redirect_uri')) {
48 | params.append('redirect_uri', config.redirectUri)
49 | }
50 |
51 | storage.removeItem(stateStorageKey)
52 | const state = customState ?? config.state
53 | if (state) {
54 | storage.setItem(stateStorageKey, state)
55 | params.append('state', state)
56 | }
57 |
58 | const loginUrl = `${config.authorizationEndpoint}?${params.toString()}`
59 |
60 | // Call any preLogin function in authConfig
61 | if (config?.preLogin) config.preLogin()
62 |
63 | if (method === 'popup') {
64 | const { width, height, left, top } = calculatePopupPosition(600, 600)
65 | const handle: null | WindowProxy = window.open(
66 | loginUrl,
67 | 'loginPopup',
68 | `width=${width},height=${height},top=${top},left=${left}`
69 | )
70 | if (handle) return
71 | console.warn('Popup blocked. Redirecting to login page. Disable popup blocker to use popup login.')
72 | }
73 | window.location[navigationMethod](loginUrl)
74 | })
75 | }
76 |
77 | // This is called a "type predicate". Which allow us to know which kind of response we got, in a type safe way.
78 | function isTokenResponse(body: unknown | TTokenResponse): body is TTokenResponse {
79 | return (body as TTokenResponse).access_token !== undefined
80 | }
81 |
82 | function postTokenRequest(
83 | tokenEndpoint: string,
84 | tokenRequest: TTokenRequest,
85 | credentials: RequestCredentials
86 | ): Promise {
87 | return postWithXForm({ url: tokenEndpoint, request: tokenRequest, credentials: credentials }).then((response) => {
88 | return response.json().then((body: TTokenResponse | unknown): TTokenResponse => {
89 | if (isTokenResponse(body)) {
90 | return body
91 | }
92 | throw Error(JSON.stringify(body))
93 | })
94 | })
95 | }
96 |
97 | export const fetchTokens = (config: TInternalConfig): Promise => {
98 | const storage = config.storage === 'session' ? sessionStorage : localStorage
99 | /*
100 | The browser has been redirected from the authentication endpoint with
101 | a 'code' url parameter.
102 | This code will now be exchanged for Access- and Refresh Tokens.
103 | */
104 | const urlParams = new URLSearchParams(window.location.search)
105 | const authCode = urlParams.get('code')
106 | // Prefix the code verifier key name to prevent multi-application collisions
107 | const codeVerifierStorageKeyName = config.storageKeyPrefix + codeVerifierStorageKey
108 | const codeVerifier = storage.getItem(codeVerifierStorageKeyName)
109 |
110 | if (!authCode) {
111 | throw Error("Parameter 'code' not found in URL. \nHas authentication taken place?")
112 | }
113 | if (!codeVerifier) {
114 | throw Error("Can't get tokens without the CodeVerifier. \nHas authentication taken place?")
115 | }
116 |
117 | const tokenRequest: TTokenRequestWithCodeAndVerifier = {
118 | grant_type: 'authorization_code',
119 | code: authCode,
120 | client_id: config.clientId,
121 | redirect_uri: config.redirectUri,
122 | code_verifier: codeVerifier,
123 | ...config.extraTokenParameters,
124 | // TODO: Remove in 2.0
125 | ...config.extraAuthParams,
126 | }
127 | return postTokenRequest(config.tokenEndpoint, tokenRequest, config.tokenRequestCredentials)
128 | }
129 |
130 | export const fetchWithRefreshToken = (props: {
131 | config: TInternalConfig
132 | refreshToken: string
133 | }): Promise => {
134 | const { config, refreshToken } = props
135 | const refreshRequest: TTokenRequestForRefresh = {
136 | grant_type: 'refresh_token',
137 | refresh_token: refreshToken,
138 | client_id: config.clientId,
139 | redirect_uri: config.redirectUri,
140 | ...config.extraTokenParameters,
141 | }
142 | if (config.refreshWithScope) refreshRequest.scope = config.scope
143 | return postTokenRequest(config.tokenEndpoint, refreshRequest, config.tokenRequestCredentials)
144 | }
145 |
146 | export function redirectToLogout(
147 | config: TInternalConfig,
148 | token: string,
149 | refresh_token?: string,
150 | idToken?: string,
151 | state?: string,
152 | logoutHint?: string,
153 | additionalParameters?: TPrimitiveRecord
154 | ) {
155 | const params = new URLSearchParams({
156 | token: refresh_token || token,
157 | token_type_hint: refresh_token ? 'refresh_token' : 'access_token',
158 | client_id: config.clientId,
159 | ui_locales: window.navigator.languages.join(' '),
160 | ...config.extraLogoutParameters,
161 | ...additionalParameters,
162 | })
163 | if (idToken) params.append('id_token_hint', idToken)
164 | if (state) params.append('state', state)
165 | if (logoutHint) params.append('logout_hint', logoutHint)
166 | if (config.logoutRedirect) params.append('post_logout_redirect_uri', config.logoutRedirect)
167 | if (!config.logoutRedirect && config.redirectUri) params.append('post_logout_redirect_uri', config.redirectUri)
168 |
169 | window.location.assign(`${config.logoutEndpoint}?${params.toString()}`)
170 | }
171 |
172 | export function validateState(urlParams: URLSearchParams, storageType: TInternalConfig['storage']) {
173 | const storage = storageType === 'session' ? sessionStorage : localStorage
174 | const receivedState = urlParams.get('state')
175 | const loadedState = storage.getItem(stateStorageKey)
176 | if (receivedState !== loadedState) {
177 | throw new Error(
178 | '"state" value received from authentication server does no match client request. Possible cross-site request forgery'
179 | )
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useEffect, useMemo, useRef, useState } from 'react'
2 | import { createInternalConfig } from './authConfig'
3 | import { fetchTokens, fetchWithRefreshToken, redirectToLogin, redirectToLogout, validateState } from './authentication'
4 | import { decodeAccessToken, decodeIdToken, decodeJWT } from './decodeJWT'
5 | import { FetchError } from './errors'
6 | import useBrowserStorage from './Hooks'
7 | import { epochAtSecondsFromNow, epochTimeIsPast, FALLBACK_EXPIRE_TIME, getRefreshExpiresIn } from './timeUtils'
8 | import type {
9 | IAuthContext,
10 | IAuthProvider,
11 | TInternalConfig,
12 | TLoginMethod,
13 | TPrimitiveRecord,
14 | TRefreshTokenExpiredEvent,
15 | TTokenResponse,
16 | } from './types'
17 |
18 | export const DEFAULT_CONTEXT_TOKEN = 'DEFAULT_CONTEXT_TOKEN'
19 |
20 | // TODO: Change to undefined context and update useAuthContext accordingly in v2
21 | export const AuthContext = createContext({
22 | token: DEFAULT_CONTEXT_TOKEN,
23 | login: () => null,
24 | logIn: () => null,
25 | logOut: () => null,
26 | error: null,
27 | loginInProgress: false,
28 | })
29 |
30 | export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
31 | const config: TInternalConfig = useMemo(() => createInternalConfig(authConfig), [authConfig])
32 |
33 | const [refreshToken, setRefreshToken] = useBrowserStorage(
34 | `${config.storageKeyPrefix}refreshToken`,
35 | undefined,
36 | config.storage
37 | )
38 | const [refreshTokenExpire, setRefreshTokenExpire] = useBrowserStorage(
39 | `${config.storageKeyPrefix}refreshTokenExpire`,
40 | undefined,
41 | config.storage
42 | )
43 | const [token, setToken] = useBrowserStorage(`${config.storageKeyPrefix}token`, '', config.storage)
44 | const [tokenExpire, setTokenExpire] = useBrowserStorage(
45 | `${config.storageKeyPrefix}tokenExpire`,
46 | epochAtSecondsFromNow(FALLBACK_EXPIRE_TIME),
47 | config.storage
48 | )
49 | const [idToken, setIdToken] = useBrowserStorage(
50 | `${config.storageKeyPrefix}idToken`,
51 | undefined,
52 | config.storage
53 | )
54 | const [loginInProgress, setLoginInProgress] = useBrowserStorage(
55 | `${config.storageKeyPrefix}loginInProgress`,
56 | false,
57 | config.storage
58 | )
59 | const [refreshInProgress, setRefreshInProgress] = useBrowserStorage(
60 | `${config.storageKeyPrefix}refreshInProgress`,
61 | false,
62 | config.storage
63 | )
64 | const [loginMethod, setLoginMethod] = useBrowserStorage(
65 | `${config.storageKeyPrefix}loginMethod`,
66 | 'redirect',
67 | config.storage
68 | )
69 | const tokenData = useMemo(() => {
70 | if (config.decodeToken) return decodeAccessToken(token)
71 | }, [token])
72 | const idTokenData = useMemo(() => decodeIdToken(idToken), [idToken])
73 | const [error, setError] = useState(null)
74 |
75 | function clearStorage() {
76 | setRefreshToken(undefined)
77 | setToken('')
78 | setTokenExpire(epochAtSecondsFromNow(FALLBACK_EXPIRE_TIME))
79 | setRefreshTokenExpire(undefined)
80 | setIdToken(undefined)
81 | setLoginInProgress(false)
82 | }
83 |
84 | function logOut(state?: string, logoutHint?: string, additionalParameters?: TPrimitiveRecord) {
85 | clearStorage()
86 | setError(null)
87 | if (config?.logoutEndpoint && token)
88 | redirectToLogout(config, token, refreshToken, idToken, state, logoutHint, additionalParameters)
89 | }
90 |
91 | function logIn(state?: string, additionalParameters?: TPrimitiveRecord, method: TLoginMethod = 'redirect') {
92 | clearStorage()
93 | setLoginInProgress(true)
94 | setLoginMethod(method)
95 | // TODO: Raise error on wrong state type in v2
96 | let typeSafePassedState = state
97 | if (state && typeof state !== 'string') {
98 | const jsonState = JSON.stringify(state)
99 | console.warn(
100 | `Passed login state must be of type 'string'. Received '${jsonState}'. Ignoring value. In a future version, an error will be thrown here.`
101 | )
102 | typeSafePassedState = undefined
103 | }
104 | redirectToLogin(config, typeSafePassedState, additionalParameters, method).catch((error) => {
105 | console.error(error)
106 | setError(error.message)
107 | setLoginInProgress(false)
108 | })
109 | }
110 |
111 | function handleTokenResponse(response: TTokenResponse) {
112 | setToken(response.access_token)
113 | if (response.id_token) {
114 | setIdToken(response.id_token)
115 | }
116 | let tokenExp = FALLBACK_EXPIRE_TIME
117 | // Decode IdToken, so we can use "exp" from that as fallback if expire not returned in the response
118 | try {
119 | if (response.id_token) {
120 | const decodedToken = decodeJWT(response.id_token)
121 | tokenExp = Math.round(Number(decodedToken.exp) - Date.now() / 1000) // number of seconds from now
122 | }
123 | } catch (e) {
124 | console.warn(`Failed to decode idToken: ${(e as Error).message}`)
125 | }
126 | const tokenExpiresIn = config.tokenExpiresIn ?? response.expires_in ?? tokenExp
127 | setTokenExpire(epochAtSecondsFromNow(tokenExpiresIn))
128 | const refreshTokenExpiresIn = config.refreshTokenExpiresIn ?? getRefreshExpiresIn(tokenExpiresIn, response)
129 | if (response.refresh_token) {
130 | setRefreshToken(response.refresh_token)
131 | if (!refreshTokenExpire || config.refreshTokenExpiryStrategy !== 'absolute') {
132 | setRefreshTokenExpire(epochAtSecondsFromNow(refreshTokenExpiresIn))
133 | }
134 | }
135 | setError(null)
136 | }
137 |
138 | function handleExpiredRefreshToken(initial = false): void {
139 | if (config.autoLogin && initial) return logIn(undefined, undefined, config.loginMethod)
140 |
141 | // TODO: Breaking change - remove automatic login during ongoing session
142 | if (!config.onRefreshTokenExpire) return logIn(undefined, undefined, config.loginMethod)
143 |
144 | config.onRefreshTokenExpire({
145 | login: logIn,
146 | logIn,
147 | } as TRefreshTokenExpiredEvent)
148 | }
149 |
150 | function refreshAccessToken(initial = false): void {
151 | if (!token) return
152 | // The token has not expired. Do nothing
153 | if (!epochTimeIsPast(tokenExpire)) return
154 |
155 | // Other instance (tab) is currently refreshing. This instance skip the refresh if not initial
156 | if (refreshInProgress && !initial) return
157 |
158 | // If no refreshToken, act as if the refreshToken expired (session expired)
159 | if (!refreshToken) return handleExpiredRefreshToken(initial)
160 |
161 | // The refreshToken has expired
162 | if (refreshTokenExpire && epochTimeIsPast(refreshTokenExpire)) return handleExpiredRefreshToken(initial)
163 |
164 | // The access_token has expired, and we have a non-expired refresh_token. Use it to refresh access_token.
165 | if (refreshToken) {
166 | setRefreshInProgress(true)
167 | fetchWithRefreshToken({ config, refreshToken })
168 | .then((result: TTokenResponse) => handleTokenResponse(result))
169 | .catch((error: unknown) => {
170 | if (error instanceof FetchError) {
171 | // If the fetch failed with status 400, assume expired refresh token
172 | if (error.status === 400) {
173 | handleExpiredRefreshToken(initial)
174 | return
175 | }
176 | // Unknown error. Set error, and log in if first page load
177 | console.error(error)
178 | setError(error.message)
179 | if (initial) logIn(undefined, undefined, config.loginMethod)
180 | }
181 | // Unknown error. Set error, and log in if first page load
182 | else if (error instanceof Error) {
183 | console.error(error)
184 | setError(error.message)
185 | if (initial) logIn(undefined, undefined, config.loginMethod)
186 | }
187 | })
188 | .finally(() => {
189 | setRefreshInProgress(false)
190 | })
191 | return
192 | }
193 | console.warn(
194 | 'Failed to refresh access_token. Most likely there is no refresh_token, or the authentication server did not reply with an explicit expire time, and the default expire times are longer than the actual tokens expire time'
195 | )
196 | }
197 |
198 | // Register the 'check for soon expiring access token' interval (every ~10 seconds).
199 | useEffect(() => {
200 | // The randomStagger is used to avoid multiple tabs logging in at the exact same time.
201 | const randomStagger = 10000 * Math.random()
202 | const interval = setInterval(() => refreshAccessToken(), 5000 + randomStagger)
203 | return () => clearInterval(interval)
204 | }, [token, refreshToken, refreshTokenExpire, tokenExpire, refreshInProgress]) // Replace the interval with a new when values used inside refreshAccessToken changes
205 |
206 | // This ref is used to make sure the 'fetchTokens' call is only made once.
207 | // Multiple calls with the same code will, and should, return an error from the API
208 | // See: https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
209 | const didFetchTokens = useRef(false)
210 |
211 | // Runs once on page load
212 | useEffect(() => {
213 | // The client has been redirected back from the auth endpoint with an auth code
214 | if (loginInProgress) {
215 | const urlParams = new URLSearchParams(window.location.search)
216 | if (!urlParams.get('code')) {
217 | // This should not happen. There should be a 'code' parameter in the url by now...
218 | const error_description =
219 | urlParams.get('error_description') ||
220 | 'Bad authorization state. Refreshing the page and log in again might solve the issue.'
221 | console.error(
222 | `${error_description}\nExpected to find a '?code=' parameter in the URL by now. Did the authentication get aborted or interrupted?`
223 | )
224 | setError(error_description)
225 | clearStorage()
226 | return
227 | }
228 | // Make sure we only try to use the auth code once
229 | if (!didFetchTokens.current) {
230 | didFetchTokens.current = true
231 | try {
232 | validateState(urlParams, config.storage)
233 | } catch (e: unknown) {
234 | console.error(e)
235 | setError((e as Error).message)
236 | }
237 | // Request tokens from auth server with the auth code
238 | fetchTokens(config)
239 | .then((tokens: TTokenResponse) => {
240 | handleTokenResponse(tokens)
241 | // Call any postLogin function in authConfig
242 | if (config?.postLogin) config.postLogin()
243 | if (loginMethod === 'popup') window.close()
244 | })
245 | .catch((error: Error) => {
246 | console.error(error)
247 | setError(error.message)
248 | })
249 | .finally(() => {
250 | if (config.clearURL) {
251 | // Clear ugly url params
252 | window.history.replaceState(null, '', `${window.location.pathname}${window.location.hash}`)
253 | }
254 | setLoginInProgress(false)
255 | })
256 | }
257 | return
258 | }
259 |
260 | // First page visit
261 | if (!token && config.autoLogin) return logIn(undefined, undefined, config.loginMethod)
262 | refreshAccessToken(true) // Check if token should be updated
263 | }, [])
264 |
265 | return (
266 |
279 | {children}
280 |
281 | )
282 | }
283 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-oauth2-code-pkce
2 | [](https://github.com/soofstad/react-oauth2-pkce/blob/main/LICENSE)    
3 |
4 | React package for OAuth2 Authorization Code flow with PKCE
5 |
6 | Adhering to the RFCs recommendations, cryptographically sound, and with __zero__ dependencies!
7 |
8 | ## What is OAuth2 Authorization Code Flow with Proof Key for Code Exchange?
9 |
10 | Short version;
11 | The modern and secure way to do authentication for mobile and web applications!
12 |
13 | Long version;
14 |
15 |
16 |
17 |
18 | ## Features
19 |
20 | - Authorization provider-agnostic. Works equally well with all OAuth2 authentication servers following the OAuth2 spec
21 | - Supports OpenID Connect (idTokens)
22 | - Pre- and Post-login callbacks
23 | - Session expired callback
24 | - Silently refreshes short-lived access tokens in the background
25 | - Decodes JWT's
26 | - A total of ~440 lines of code, easy for anyone to audit and understand
27 |
28 | ## Example
29 |
30 | ```tsx
31 | import { useAuthContext, AuthProvider, TAuthConfig, TRefreshTokenExpiredEvent } from "react-oauth2-code-pkce"
32 |
33 | const authConfig: TAuthConfig = {
34 | clientId: 'myClientID',
35 | authorizationEndpoint: 'https://myAuthProvider.com/auth',
36 | tokenEndpoint: 'https://myAuthProvider.com/token',
37 | redirectUri: 'http://localhost:3000/',
38 | scope: 'someScope openid',
39 | onRefreshTokenExpire: (event: TRefreshTokenExpiredEvent) => event.logIn(undefined, undefined, "popup"),
40 | }
41 |
42 | const UserInfo = (): JSX.Element => {
43 | const {token, tokenData} = useAuthContext()
44 |
45 | return <>
46 |
Access Token
47 |
{token}
48 |
User Information from JWT
49 |
{JSON.stringify(tokenData, null, 2)}
50 | >
51 | }
52 |
53 | ReactDOM.render(
54 |
55 |
56 | , document.getElementById('root'),
57 | )
58 | ```
59 |
60 | For more advanced examples, see `./examples/`.
61 |
62 | ## Install
63 |
64 | The package is available on npmjs.com here; https://www.npmjs.com/package/react-oauth2-code-pkce
65 |
66 | ```bash
67 | npm install react-oauth2-code-pkce
68 | ```
69 |
70 | ## API
71 |
72 | ### IAuthContext values
73 |
74 | The object that's returned by `useAuthContext()` provides these values;
75 |
76 | ```typescript
77 | interface IAuthContext {
78 | // The access token. This is what you will use for authentication against protected Web API's
79 | token: string
80 | // An object with all the properties encoded in the token (username, email, etc.), if the token is a JWT
81 | tokenData?: TTokenData
82 | // Function to trigger login.
83 | // If you want to use 'state', you might want to set 'clearURL' configuration parameter to 'false'.
84 | // Note that most browsers block popups by default. The library will print a warning and fallback to redirect if the popup is blocked
85 | logIn: (state?: string, additionalParameters?: { [key: string]: string | boolean | number }, method: TLoginMethod = 'redirect') => void
86 | // Function to trigger logout from authentication provider. You may provide optional 'state', and 'logout_hint' values.
87 | // See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout for details.
88 | logOut: (state?: string, logoutHint?: string, additionalParameters?: { [key: string]: string | boolean | number }) => void
89 | // Keeps any errors that occured during login, token fetching/refreshing, decoding, etc..
90 | error: string | null
91 | // The idToken, if it was returned along with the access token
92 | idToken?: string
93 | // An object with all the properties encoded in the ID-token (username, groups, etc.)
94 | idTokenData?: TTokenData
95 | // If the is done fetching tokens or not. Usefull for controlling page rendering
96 | loginInProgress: boolean
97 | }
98 | ```
99 |
100 | ### Configuration parameters
101 |
102 | __react-oauth2-code-pkce__'s goal is to "just work" with any authentication provider that either
103 | supports the [OAuth2](https://datatracker.ietf.org/doc/html/rfc7636) or [OpenID Connect](https://openid.net/developers/specs/) (OIDC) standards.
104 | However, many authentication providers are not following these standards, or have extended them.
105 | 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.
106 | If they do, these can be injected into the different calls with these configuration options;
107 |
108 | - `extraAuthParameters`
109 | - `extraTokenParameters`
110 | - `extraLogoutParameters`
111 |
112 | The `` takes a `config` object that supports these parameters;
113 |
114 | ```typescript
115 | type TAuthConfig = {
116 | // ID of your app at the authentication provider
117 | clientId: string // Required
118 | // URL for the authentication endpoint at the authentication provider
119 | authorizationEndpoint: string // Required
120 | // URL for the token endpoint at the authentication provider
121 | tokenEndpoint: string // Required
122 | // Which URL the auth provider should redirect the user to after successful authentication/login
123 | // NOTE: Even if it is declared as optional in the RFC, most identity providers will require it to be set
124 | redirectUri?: string // default: undefined
125 | // Which scopes to request for the auth token
126 | scope?: string // default: ''
127 | // Optional state value. Will often make more sense to provide the state in a call to the 'logIn()' function
128 | state?: string // default: null
129 | // Which URL to call for logging out of the auth provider
130 | logoutEndpoint?: string // default: null
131 | // Which URL the auth provider should redirect the user to after logout
132 | logoutRedirect?: string // default: null
133 | // Optionally provide a callback function to run _before_ the
134 | // user is redirected to the auth server for login
135 | preLogin?: () => void // default: () => null
136 | // Optionally provide a callback function to run _after_ the
137 | // user has been redirected back from the auth server
138 | postLogin?: () => void // default: () => null
139 | // Which method to use for login. Can be 'redirect', 'replace', or 'popup'
140 | // Note that most browsers block popups by default. The library will print a warning and fallback to redirect if the popup is blocked
141 | loginMethod: 'redirect' | 'replace' | 'popup' // default: 'redirect'
142 | // Optional callback function for the 'refreshTokenExpired' event.
143 | // You likely want to display a message saying the user need to log in again. A page refresh is enough.
144 | onRefreshTokenExpire?: (event: TRefreshTokenExpiredEvent) => void // default: undefined
145 | // Whether or not to decode the access token (should be set to 'false' if the access token is not a JWT (e.g. from Github))
146 | // If `false`, 'tokenData' will be 'undefined' from the
147 | decodeToken?: boolean // default: true
148 | // By default, the package will automatically redirect the user to the login server if not already logged in.
149 | // If set to false, you need to call the "logIn()" function to log in (e.g. with a "Log in" button)
150 | autoLogin?: boolean // default: true
151 | // Store login state in 'localStorage' or 'sessionStorage'
152 | // If set to 'session', no login state is persisted by 'react-oauth2-code-pkce` when the browser closes.
153 | // NOTE: Many authentication servers will keep the client logged in by cookies. You should therefore use
154 | // the logOut() function to properly log out the client. Or configure your server not to issue cookies.
155 | storage?: 'local' | 'session' // default: 'local'
156 | // Sets the prefix for keys used by this library in storage
157 | storageKeyPrefix?: string // default: 'ROCP_'
158 | // Set to false if you need to access the urlParameters sent back from the login server.
159 | clearURL?: boolean // default: true
160 | // Can be used to provide any non-standard parameters to the authentication request
161 | extraAuthParameters?: { [key: string]: string | boolean | number } // default: null
162 | // Can be used to provide any non-standard parameters to the token request
163 | extraTokenParameters?: { [key: string]: string | boolean | number } // default: null
164 | // Can be used to provide any non-standard parameters to the logout request
165 | extraLogoutParameters?: { [key: string]: string | boolean | number } // default: null
166 | // Superseded by 'extraTokenParameters' options. Will be deprecated in 2.0
167 | extraAuthParams?: { [key: string]: string | boolean | number } // default: null
168 | // Can be used if auth provider doesn't return access token expiration time in token response
169 | tokenExpiresIn?: number // default: null
170 | // Can be used if auth provider doesn't return refresh token expiration time in token response
171 | refreshTokenExpiresIn?: number // default: null
172 | // Defines the expiration strategy for the refresh token.
173 | // - 'renewable': The refresh token's expiration time is renewed each time it is used, getting a new validity period.
174 | // - 'absolute': The refresh token's expiration time is fixed from its initial issuance and does not change, regardless of how many times it is used.
175 | refreshTokenExpiryStrategy?: 'renewable' | 'absolute' // default: renewable
176 | // Whether or not to post 'scope' when refreshing the access token
177 | refreshWithScope?: boolean // default: true
178 | // Controls whether browser credentials (cookies, TLS client certificates, or authentication headers containing a username and password) are sent when requesting tokens.
179 | // Warning: Including browser credentials deviates from the standard protocol and can introduce unforeseen security issues. Only set this to 'include' if you know what
180 | // 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
181 | // not needed in any other case. Use with caution.
182 | tokenRequestCredentials?: 'same-origin' | 'include' | 'omit' // default: 'same-origin'
183 | }
184 |
185 | ```
186 |
187 | ## Common issues
188 |
189 | ### Sessions expire too quickly
190 |
191 | A session expire happens when the `refresh_token` is no longer valid and can't be used to fetch a new valid `access_token`.
192 | This is governed by the `expires_in`, and `refresh_expires_in | refresh_token_expires_in`, in the token response.
193 | If the response does not contain these values, the library assumes a quite conservative value.
194 | You should configure your IDP (Identity Provider) to send these, but if that is not possible, you can set them explicitly
195 | with the config parameters `tokenExpiresIn` and `refreshTokenExpiresIn`.
196 |
197 | ### Fails to compile with Next.js
198 | 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).
199 |
200 | This can be solved by marking the module with `use client` and importing the component in the client only (`"ssr": false`).
201 |
202 | ```tsx
203 | 'use client'
204 | import dynamic from 'next/dynamic'
205 | import {TAuthConfig, TRefreshTokenExpiredEvent, useAuthContext} from 'react-oauth2-code-pkce'
206 |
207 | const AuthProvider = dynamic(
208 | ()=> import("react-oauth2-code-pkce")
209 | .then((mod) => mod.AuthProvider),
210 | {ssr: false}
211 | )
212 |
213 | const authConfig: TAuthConfig = {...for you to fill inn}
214 |
215 | export default function Authenticated() {
216 | (
217 |
218 | )
219 | }
220 | ```
221 |
222 | ### Error `Bad authorization state...`
223 |
224 | This is most likely to happen if the authentication at the identity provider got aborted in some way.
225 | 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.
226 |
227 | First of all, you should handle any errors the library throws. Usually, hinting at the user reload the page is enough.
228 |
229 | Some known causes for this is that instead of logging in at the auth provider, the user "Registers" or "Reset password" or
230 | something similar instead. Any such functions should be handled outside of this library, with separate buttons/links than the "Log in" button.
231 |
232 | ### After redirect back from auth provider with `?code`, no token request is made
233 |
234 | If you are using libraries that intercept any `fetch()`-requests made. For example `@tanstack/react-query`. That can cause
235 | issues for the _AuthProviders_ token fetching. This can be solved by _not_ wrapping the `` in any such library.
236 |
237 | This could also happen if some routes in your app are not wrapped by the ``.
238 |
239 | ### The page randomly refreshes in the middle of a session
240 |
241 | This will happen if you haven't provided a callback-function for the `onRefreshTokenExpire` config parameter, and the refresh token expires.
242 | 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.
243 | Either by refreshing the page, or clicking a "Log in" button.
244 |
245 | ## Develop
246 |
247 | 1. Update the 'authConfig' object in `src/index.js` with config from your authorization server and application
248 | 2. Install node_modules -> `$ yarn install`
249 | 3. Run -> `$ yarn start`
250 |
251 | ## Contribute
252 |
253 | You are most welcome to create issues and pull requests :)
254 |
--------------------------------------------------------------------------------
/examples/github-auth-provider/github-auth-proxy/poetry.lock:
--------------------------------------------------------------------------------
1 | [[package]]
2 | name = "anyio"
3 | version = "3.6.1"
4 | description = "High level compatibility layer for multiple asynchronous event loop implementations"
5 | category = "main"
6 | optional = false
7 | python-versions = ">=3.6.2"
8 |
9 | [package.dependencies]
10 | idna = ">=2.8"
11 | sniffio = ">=1.1"
12 |
13 | [package.extras]
14 | doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
15 | test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
16 | trio = ["trio (>=0.16)"]
17 |
18 | [[package]]
19 | name = "certifi"
20 | version = "2022.6.15"
21 | description = "Python package for providing Mozilla's CA Bundle."
22 | category = "main"
23 | optional = false
24 | python-versions = ">=3.6"
25 |
26 | [[package]]
27 | name = "charset-normalizer"
28 | version = "2.1.0"
29 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
30 | category = "main"
31 | optional = false
32 | python-versions = ">=3.6.0"
33 |
34 | [package.extras]
35 | unicode_backport = ["unicodedata2"]
36 |
37 | [[package]]
38 | name = "click"
39 | version = "8.1.3"
40 | description = "Composable command line interface toolkit"
41 | category = "main"
42 | optional = false
43 | python-versions = ">=3.7"
44 |
45 | [package.dependencies]
46 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
47 |
48 | [[package]]
49 | name = "colorama"
50 | version = "0.4.5"
51 | description = "Cross-platform colored terminal text."
52 | category = "main"
53 | optional = false
54 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
55 |
56 | [[package]]
57 | name = "fastapi"
58 | version = "0.78.0"
59 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
60 | category = "main"
61 | optional = false
62 | python-versions = ">=3.6.1"
63 |
64 | [package.dependencies]
65 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
66 | starlette = "0.19.1"
67 |
68 | [package.extras]
69 | all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
70 | dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)", "pre-commit (>=2.17.0,<3.0.0)"]
71 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.0.0)"]
72 | test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"]
73 |
74 | [[package]]
75 | name = "h11"
76 | version = "0.13.0"
77 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
78 | category = "main"
79 | optional = false
80 | python-versions = ">=3.6"
81 |
82 | [[package]]
83 | name = "idna"
84 | version = "3.3"
85 | description = "Internationalized Domain Names in Applications (IDNA)"
86 | category = "main"
87 | optional = false
88 | python-versions = ">=3.5"
89 |
90 | [[package]]
91 | name = "pydantic"
92 | version = "1.9.1"
93 | description = "Data validation and settings management using python type hints"
94 | category = "main"
95 | optional = false
96 | python-versions = ">=3.6.1"
97 |
98 | [package.dependencies]
99 | typing-extensions = ">=3.7.4.3"
100 |
101 | [package.extras]
102 | dotenv = ["python-dotenv (>=0.10.4)"]
103 | email = ["email-validator (>=1.0.3)"]
104 |
105 | [[package]]
106 | name = "python-multipart"
107 | version = "0.0.5"
108 | description = "A streaming multipart parser for Python"
109 | category = "main"
110 | optional = false
111 | python-versions = "*"
112 |
113 | [package.dependencies]
114 | six = ">=1.4.0"
115 |
116 | [[package]]
117 | name = "requests"
118 | version = "2.28.1"
119 | description = "Python HTTP for Humans."
120 | category = "main"
121 | optional = false
122 | python-versions = ">=3.7, <4"
123 |
124 | [package.dependencies]
125 | certifi = ">=2017.4.17"
126 | charset-normalizer = ">=2,<3"
127 | idna = ">=2.5,<4"
128 | urllib3 = ">=1.21.1,<1.27"
129 |
130 | [package.extras]
131 | socks = ["PySocks (>=1.5.6,!=1.5.7)"]
132 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
133 |
134 | [[package]]
135 | name = "six"
136 | version = "1.16.0"
137 | description = "Python 2 and 3 compatibility utilities"
138 | category = "main"
139 | optional = false
140 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
141 |
142 | [[package]]
143 | name = "sniffio"
144 | version = "1.2.0"
145 | description = "Sniff out which async library your code is running under"
146 | category = "main"
147 | optional = false
148 | python-versions = ">=3.5"
149 |
150 | [[package]]
151 | name = "starlette"
152 | version = "0.19.1"
153 | description = "The little ASGI library that shines."
154 | category = "main"
155 | optional = false
156 | python-versions = ">=3.6"
157 |
158 | [package.dependencies]
159 | anyio = ">=3.4.0,<5"
160 |
161 | [package.extras]
162 | full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"]
163 |
164 | [[package]]
165 | name = "typing-extensions"
166 | version = "4.3.0"
167 | description = "Backported and Experimental Type Hints for Python 3.7+"
168 | category = "main"
169 | optional = false
170 | python-versions = ">=3.7"
171 |
172 | [[package]]
173 | name = "urllib3"
174 | version = "1.26.10"
175 | description = "HTTP library with thread-safe connection pooling, file post, and more."
176 | category = "main"
177 | optional = false
178 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
179 |
180 | [package.extras]
181 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
182 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
183 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
184 |
185 | [[package]]
186 | name = "uvicorn"
187 | version = "0.18.2"
188 | description = "The lightning-fast ASGI server."
189 | category = "main"
190 | optional = false
191 | python-versions = ">=3.7"
192 |
193 | [package.dependencies]
194 | click = ">=7.0"
195 | h11 = ">=0.8"
196 |
197 | [package.extras]
198 | standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchfiles (>=0.13)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"]
199 |
200 | [metadata]
201 | lock-version = "1.1"
202 | python-versions = "^3.10"
203 | content-hash = "e9d8373fb081d533580f23cbb046fbabbfb7459badbbcb2408bcc0fec123cab1"
204 |
205 | [metadata.files]
206 | anyio = [
207 | {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
208 | {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
209 | ]
210 | certifi = [
211 | {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
212 | {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
213 | ]
214 | charset-normalizer = [
215 | {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
216 | {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},
217 | ]
218 | click = [
219 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
220 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
221 | ]
222 | colorama = [
223 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
224 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
225 | ]
226 | fastapi = [
227 | {file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"},
228 | {file = "fastapi-0.78.0.tar.gz", hash = "sha256:3233d4a789ba018578658e2af1a4bb5e38bdd122ff722b313666a9b2c6786a83"},
229 | ]
230 | h11 = [
231 | {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"},
232 | {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"},
233 | ]
234 | idna = [
235 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
236 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
237 | ]
238 | pydantic = [
239 | {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"},
240 | {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"},
241 | {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"},
242 | {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"},
243 | {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"},
244 | {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"},
245 | {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"},
246 | {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"},
247 | {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"},
248 | {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"},
249 | {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"},
250 | {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"},
251 | {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"},
252 | {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"},
253 | {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"},
254 | {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"},
255 | {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"},
256 | {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"},
257 | {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"},
258 | {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"},
259 | {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"},
260 | {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"},
261 | {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"},
262 | {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"},
263 | {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"},
264 | {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"},
265 | {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"},
266 | {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"},
267 | {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"},
268 | {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"},
269 | {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"},
270 | {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"},
271 | {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"},
272 | {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"},
273 | {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"},
274 | ]
275 | python-multipart = [
276 | {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"},
277 | ]
278 | requests = [
279 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
280 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
281 | ]
282 | six = [
283 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
284 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
285 | ]
286 | sniffio = [
287 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
288 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
289 | ]
290 | starlette = [
291 | {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"},
292 | {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"},
293 | ]
294 | typing-extensions = [
295 | {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
296 | {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
297 | ]
298 | urllib3 = []
299 | uvicorn = []
300 |
--------------------------------------------------------------------------------