Skip to content

Commit

Permalink
Add example for github. Handle no refresh_token and no expire
Browse files Browse the repository at this point in the history
  • Loading branch information
soofstad committed Jul 12, 2022
1 parent e690269 commit 43b746a
Show file tree
Hide file tree
Showing 21 changed files with 206 additions and 111 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ node_modules
log.txt
/yarn.lock
/src/react-app-env.d.ts
/tsconfig.json
/src/yarn.lock
.env
__pycache__
20 changes: 20 additions & 0 deletions examples/github-auth-provider/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: "3.8"

services:
api:
build: ./github-auth-proxy
restart: unless-stopped
volumes:
- ./github-auth-proxy:/code
environment:
CLIENT_ID: c43524cc7d3c82b05a47
CLIENT_SECRET: ${CLIENT_SECRET}
ports:
- "5000:5000"
web:
build: ./web-app
restart: unless-stopped
volumes:
- ./web-app/src:/app/src
ports:
- "3000:3000"
19 changes: 19 additions & 0 deletions examples/github-auth-provider/github-auth-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.10-slim
WORKDIR /code
EXPOSE 5000

ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/code

RUN pip install --upgrade pip && \
pip install poetry && \
poetry config virtualenvs.create false

COPY pyproject.toml pyproject.toml
COPY poetry.lock poetry.lock

RUN poetry install
COPY . .
USER 1000
CMD ["python", "api.py"]

49 changes: 49 additions & 0 deletions examples/github-auth-provider/github-auth-proxy/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os

import requests
import uvicorn
from fastapi import FastAPI, Form
from fastapi.middleware.cors import CORSMiddleware

origins = [
"http://localhost:3000",
]


def create_app() -> FastAPI:
app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

@app.post("/api/token")
def get_github_token(code: str = Form()):
parameters = {
"client_id": os.getenv("CLIENT_ID"),
"client_secret": os.getenv("CLIENT_SECRET"),
"code": code
}
response = requests.post(
"https://github.com/login/oauth/access_token",
params=parameters,
headers={"Accept": "application/json"}
)
response.raise_for_status()
return response.json()

return app


if __name__ == "__main__":
uvicorn.run(
"api:create_app",
host="0.0.0.0",
port=5000,
reload=True,
log_level='debug',
)
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"dependencies": {
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-oauth2-code-pkce": "^1.2.4",
"react-oauth2-code-pkce": "^1.3.0-alpha.1",
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "PORT=80 react-scripts start",
"start": "react-scripts start",
"build": "react-scripts build"
},
"devDependencies": {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import ReactDOM from 'react-dom'
import { AuthContext, AuthProvider, TAuthConfig, IAuthContext } from "react-oauth2-code-pkce"

const authConfig: TAuthConfig = {
clientId: 'f462a430-56f0-4a00-800a-6f578da7e943',
authorizationEndpoint: 'https://login.microsoftonline.com/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/oauth2/v2.0/authorize',
tokenEndpoint: 'https://login.microsoftonline.com/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/oauth2/v2.0/token',
scope: 'User.Read',
redirectUri: 'http://localhost/',
logoutEndpoint: '',
logoutRedirect: '',
clientId: 'c43524cc7d3c82b05a47',
authorizationEndpoint: 'https://github.com/login/oauth/authorize',
tokenEndpoint: 'http://localhost:5000/api/token',
redirectUri: 'http://localhost:3000/',
// Example to redirect back to original path after login has completed
preLogin: () => localStorage.setItem('preLoginPath', window.location.pathname),
postLogin: () => window.location.replace(localStorage.getItem('preLoginPath') || ''),
decodeToken: false,
}

function LoginInfo(): JSX.Element {
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "PORT=80 react-scripts start ./src/index.js",
"start": "react-scripts start ./src/index.js",
"build": "react-scripts build",
"test": "jest"
},
Expand Down
62 changes: 38 additions & 24 deletions src/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,30 @@ import {
errorMessageForExpiredRefreshToken,
fetchTokens,
fetchWithRefreshToken,
login,
logIn,
timeOfExpire,
tokenExpired,
} from "./authentication"
} from './authentication'
import useLocalStorage from './Hooks'
import { IAuthContext, IAuthProvider, TTokenData, TInternalConfig, TTokenResponse } from './Types'
import { IAuthContext, IAuthProvider, TInternalConfig, TTokenData, TTokenResponse } from './Types'
import { validateAuthConfig } from './validateAuthConfig'

const FALLBACK_EXPIRE_TIME = 600 // 10minutes

export const AuthContext = createContext<IAuthContext>({
token: '',
logOut: () => null,
error: null,
})

export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
const [refreshToken, setRefreshToken] = useLocalStorage<string | null>('ROCP_refreshToken', null)
const [refreshToken, setRefreshToken] = useLocalStorage<string | undefined>('ROCP_refreshToken', undefined)
const [refreshTokenExpire, setRefreshTokenExpire] = useLocalStorage<number>(
'ROCP_refreshTokenExpire',
timeOfExpire(FALLBACK_EXPIRE_TIME)
)
const [token, setToken] = useLocalStorage<string>('ROCP_token', '')
const [tokenExpire, setTokenExpire] = useLocalStorage<string | null>('ROCP_tokenExpire', null)
const [tokenExpire, setTokenExpire] = useLocalStorage<number>('ROCP_tokenExpire', timeOfExpire(FALLBACK_EXPIRE_TIME))
const [idToken, setIdToken] = useLocalStorage<string | undefined>('ROCP_idToken', undefined)
const [loginInProgress, setLoginInProgress] = useLocalStorage<boolean>('ROCP_loginInProgress', false)
const [tokenData, setTokenData] = useState<TTokenData | undefined>()
Expand All @@ -30,12 +36,7 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
let interval: any

// Set default values and override from passed config
const {
decodeToken = true,
scope = "",
preLogin = () => null,
postLogin = () => null,
} = authConfig
const { decodeToken = true, scope = '', preLogin = () => null, postLogin = () => null } = authConfig

const config: TInternalConfig = {
decodeToken: decodeToken,
Expand All @@ -48,39 +49,46 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
validateAuthConfig(config)

function logOut() {
setRefreshToken(null)
setRefreshToken(undefined)
setToken('')
setTokenExpire(timeOfExpire(FALLBACK_EXPIRE_TIME))
setRefreshTokenExpire(timeOfExpire(FALLBACK_EXPIRE_TIME))
setIdToken(undefined)
setTokenData(undefined)
setLoginInProgress(false)
}

function handleTokenResponse(response: TTokenResponse) {
setRefreshToken(response.refresh_token)
setRefreshToken(response?.refresh_token)
setToken(response.access_token)
setTokenExpire(timeOfExpire(response.expires_in))
setTokenExpire(timeOfExpire(response.expires_in || FALLBACK_EXPIRE_TIME))
setRefreshTokenExpire(timeOfExpire(response.refresh_token_expires_in || FALLBACK_EXPIRE_TIME))
setIdToken(response?.id_token)
setLoginInProgress(false)
if (config.decodeToken) setTokenData(decodeJWT(response.access_token))
try {
if (config.decodeToken) setTokenData(decodeJWT(response.access_token))
} catch (e) {
setError((e as Error).message)
}
}

function refreshAccessToken() {
if (refreshToken) {
if (token && tokenExpired(tokenExpire)) { // The client has an expired token. Will try to get a new one with the refreshToken
if (token && tokenExpired(tokenExpire)) {
if (refreshToken && !tokenExpired(refreshTokenExpire)) {
fetchWithRefreshToken({ config, refreshToken })
.then((result: any) => handleTokenResponse(result))
.catch((error: string) => {
setError(error)
if (errorMessageForExpiredRefreshToken(error)) {
logOut()
login(config)
logIn(config)
}
})
} else {
// The refresh token has expired. Need to log in from scratch.
logOut()
logIn(config)
}
} else {
// No refresh_token
console.error('Tried to refresh access_token without a refresh_token.')
setError('Bad authorization state. Refreshing the page might solve the issue.')
}
}

Expand Down Expand Up @@ -118,9 +126,15 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
} else if (!token) {
// First page visit
setLoginInProgress(true)
login(config)
logIn(config)
} else {
if (decodeToken) setTokenData(decodeJWT(token))
if (decodeToken) {
try {
setTokenData(decodeJWT(token))
} catch (e) {
setError((e as Error).message)
}
}
refreshAccessToken() // Check if token should be updated
}
}, []) // eslint-disable-line
Expand Down
4 changes: 2 additions & 2 deletions src/Hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { useState } from 'react'

function useLocalStorage<T>(key: string, initialValue: T): [T, (v: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
const item = localStorage.getItem(key)
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
return initialValue
Expand All @@ -16,7 +16,7 @@ function useLocalStorage<T>(key: string, initialValue: T): [T, (v: T) => void] {
setStoredValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.log(error)
console.log(`Failed to store value '${value}' for key '${key}'`)
}
}

Expand Down
28 changes: 10 additions & 18 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ export type TTokenData = {

export type TTokenResponse = {
access_token: string
expires_in: number
scope: string
token_type: string
expires_in?: number
refresh_token?: string
refresh_token_expires_in?: number
id_token?: string
}

Expand All @@ -27,10 +28,9 @@ export interface IAuthContext {
idToken?: string
}


// Input from users of the package, some optional values
export type TAuthConfig = {
clientId: string
clientId: string
authorizationEndpoint: string
tokenEndpoint: string
redirectUri: string
Expand All @@ -39,30 +39,22 @@ clientId: string
logoutRedirect?: string
preLogin?: () => void
postLogin?: () => void
postLogin?: () => void
decodeToken?: boolean
}

export type TTokenResponse = {
access_token: string
refresh_token: string
expires_in: number
id_token?: string
}

export type TAzureADErrorResponse = {
error_description: string
[k: string]: unknown
}

// The AuthProviders internal config type. All values will be set by user provided, or default values
export type TInternalConfig = {
clientId: string
authorizationEndpoint: string
clientId: string
authorizationEndpoint: string
tokenEndpoint: string
redirectUri: string
scope: string
preLogin: Function
postLogin: Function
redirectUri: string
scope: string
preLogin?: () => void
postLogin?: () => void
decodeToken: boolean
}
}
Loading

0 comments on commit 43b746a

Please sign in to comment.