-
-
Save blubbel01/d3ff114356b1e3aebaf6f543fe0474ff to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import express, {Express, Request, Response} from 'express'; | |
import crypto from 'crypto'; | |
import { ParsedQs } from "qs"; | |
import axios from 'axios'; | |
import jwt, { JwtPayload } from 'jsonwebtoken'; | |
const APP_AUTH_URI = 'https://apiv1.vio-v.com/api/oauth2/authorize'; | |
const APP_TOKEN_URI = 'https://apiv1.vio-v.com/api/oauth2/token'; | |
const TOKEN_ISSUER = 'https://apiv1.vio-v.com' | |
// The data provided is only an example and SHALL be adjusted on use | |
const APP_REDIRECT_URI = 'https://example.com/login/callback'; | |
const CLIENT_ID = 'T3zS5RvwTudXfJiVMektrQXAvVC2rTjidp7SEbtl'; | |
const CLIENT_SECRET = ' 9s7WWoQM-1iYi-s1X8zSIlYPXeq7jV-mR5ToQ_fba5YKKcNr9O7cQGZPoBLzd4o1BcoRHtllqFksSgdqtCnzaas7u81TNryTmyBJ-LpZcq5ejaWx1v6FUOg98ca3KFpZtwTncufljMIzaEJ1RgovvHiELkidgBiU0jNj6s8t1-8 '; // only for non-public clients | |
declare module 'express-session' { | |
interface SessionData { | |
code_verifier?: string | |
state?: string | |
} | |
} | |
const app: Express = express(); | |
interface ICallbackQuery extends ParsedQs { | |
code: string | |
state: string | |
iss: string // should be https://apiv1.vio-v.com | |
} | |
interface ITokenResponse { | |
access_token: string | |
token_type: string | |
expires_in: number | |
refresh_token: string | |
} | |
app.get('/login', (req: Request, res: Response) => { | |
// access granting the access_token and refresh_token to this client | |
const scopes = [ | |
'read.self.info', | |
'read.group.info', | |
'read.group.members' | |
] | |
// PKCE Steps as defined in RFC 7636 | |
const code_verifier = crypto.randomBytes(60).toString('ascii'); | |
const code_challenge = crypto.createHash('SHA256').update(code_verifier).digest('base64url'); | |
req.session.code_verifier = code_verifier; | |
const state = crypto.randomBytes(25).toString('base64url'); | |
req.session.state = state; | |
const redirectUrl = new URL(APP_AUTH_URI); | |
redirectUrl.searchParams.append('code_challenge', code_challenge); | |
redirectUrl.searchParams.append('code_challenge_method', 'S256'); | |
redirectUrl.searchParams.append('response_type', 'code'); | |
redirectUrl.searchParams.append('client_id', CLIENT_ID); | |
redirectUrl.searchParams.append('scope', scopes.join(' ')); | |
redirectUrl.searchParams.append('state', state); | |
redirectUrl.searchParams.append('redirect_uri', APP_REDIRECT_URI); | |
res.redirect(redirectUrl.toString()); | |
}); | |
app.get('/login/callback', async (req: Request, res: Response) => { | |
const { | |
code, | |
state, | |
iss, | |
error, | |
} = <ICallbackQuery> req.query; | |
if (req.session.code_verifier == null || req.session.state == null || iss !== TOKEN_ISSUER) { | |
res.sendStatus(500); | |
return; | |
} | |
if (state !== req.session.state) { | |
res.sendStatus(500); | |
return; | |
} | |
if(error != null) { | |
// Handle error | |
// error content is as defined in RFC 6749 | |
return; | |
} | |
try { | |
const params = new URLSearchParams(); | |
params.append('grant_type', 'authorization_code'); | |
params.append('code', code); | |
params.append('code_verifier', req.session.code_verifier); | |
params.append('redirect_uri', APP_REDIRECT_URI); | |
params.append('client_id', CLIENT_ID); | |
params.append('client_secret', CLIENT_SECRET); // do not send if public client !! | |
const response = await axios.post<ITokenResponse>(APP_TOKEN_URI, params); | |
// save access_token and refresh_token somewhere | |
// detailed descreption in RFC 6749 | |
} catch(_) { | |
// response status is not 200 | |
res.sendStatus(500); | |
return; | |
} | |
}); | |
/* | |
* Get valid access_token and if necessary regenerate using refresh_token | |
*/ | |
async function getAccessToken(someLogic: any): Promise<string> { | |
// somehow get your refresh_token and access_token | |
const access_token = ''; | |
const refresh_token = ''; | |
const accessTokenData = jwt.decode(access_token, { | |
json: true | |
}); | |
// ist token abgelaufen oder fehlerhaft | |
if (accessTokenData?.exp! < Math.floor(Date.now() / 1000) || accessTokenData == null) { | |
const params = new URLSearchParams(); | |
params.append('grant_type', 'refresh_token'); | |
params.append('refresh_token', refresh_token); | |
params.append('client_id', CLIENT_ID); | |
params.append('client_secret', CLIENT_SECRET); // do not send if public client !! | |
// this is optinal, only send if you want to limit the scopes on the access_token. can only grant scopes than already refresh_token has | |
// if not needed delete param scope and access_token will have same scope as refresh_token | |
params.append('scope', [ | |
'read.self.info' | |
].join(' ')); | |
const response = await axios.post<ITokenResponse>(APP_TOKEN_URI, params); | |
// save access_token and refresh_token somewhere | |
// you have to save refresh_token as the same refresh_token can not be used twice | |
// detailed descreption in RFC 6749 | |
return response.data.access_token; | |
} else { | |
return access_token; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment