Skip to content

OAuth2: oauth2AccessTokenProvider(providers)() #26

@vlaurin

Description

@vlaurin

Improved OAuth2 access token provider with caching and support for refresh token; which do not rely on asynchronous cache invalidation.

Code

const DEFAULT_LATENCY_MS = 1500;

export const oauth2AccessTokenProvider = ({newTokensProvider, refreshedTokensProvider}) => withNewSession(provideAccessToken({
    startSession: sessionStarter(newTokensProvider),
    refreshSession: refreshedTokensProvider && sessionRefresher(refreshedTokensProvider),
}));

const withNewSession = (fn) => fn({});

const provideAccessToken = ({startSession, refreshSession}) => (session) => () => (isValid(session) ? getAccessToken : refreshSession && canRefresh(session) ? refreshSession : startSession)(session);

const updateSession = (session) => (tokens) => Object.assign(session, tokens, expiryTimes(tokens));

const expiryTimes = (tokens) => ({
    expires_at: expireAt(tokens.expires_in),
    refresh_expires_at: expireAt(tokens.refresh_expires_in),
});

const sToMs = (x) => x * 1000;
const withLatency = (x) => x - DEFAULT_LATENCY_MS;
const expireAt = (durationSeconds) => withLatency(Date.now() + sToMs(durationSeconds));

const isFuture = (timestampMs) => timestampMs && Date.now() < timestampMs;
const isValid = (session) => isFuture(session.expires_at);
const canRefresh = (session) => isFuture(session.refresh_expires_at);

const sessionStarter = (tokensProvider) => (session) => tokensProvider().then(updateSession(session)).then(getAccessToken);
const sessionRefresher = (tokensProvider) => (session) => tokensProvider(getRefreshToken(session)).then(updateSession(session)).then(getAccessToken);

const getAccessToken = (session) => Promise.resolve(session.access_token);
const getRefreshToken = (session) => session.refresh_token;

Example

import axios from 'axios';
import {oauth2AccessTokenProvider} from '@quickcase/node-toolkit';

// FIXME use config
const tokenEndpoint = 'http://keycloak:8080/auth/realms/master/protocol/openid-connect/token';
const username = 'quickcase';
const password = 'pass';

const urlSearchParams = (params) => new URLSearchParams(params);
const extractData = (res) => res.data;

const newTokensProvider = () => axios.post(tokenEndpoint, urlSearchParams({
    grant_type: 'password',
    client_id: 'admin-cli',
    username,
    password,
})).then(extractData);

const refreshedTokensProvider = (refreshToken) => axios.post(tokenEndpoint, urlSearchParams({
    grant_type: 'refresh_token',
    client_id: 'admin-cli',
    refresh_token: refreshToken,
})).then(extractData);

const tokenProvider = oauth2AccessTokenProvider({
    newTokensProvider,
    refreshedTokensProvider,
});

let c = '';
setInterval(() => {
    tokenProvider().then(token => {
        if (c === token) {
            console.log(`${new Date().toISOString()} ---`);
        } else {
            console.log(`${new Date().toISOString()} NEW`);
        }
        c = token;
    });
}, 5000);

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions