import * as OAuthServer from "oauth2-server";
import * as winston from "winston";
import * as express from "express";
import { TokenUser, Base, NoderedUtil } from "@openiap/openflow-api";
import { Config } from "./Config";
import { Crypt } from "./Crypt";
const Request = OAuthServer.Request;
const Response = OAuthServer.Response;
export class OAuthProvider {
private _logger: winston.Logger;
private app: express.Express;
public static instance: OAuthProvider = null;
private clients = [];
private codes = {};
public oauthServer: any = null;
private authorizationCodeStore: any = {};
static configure(logger: winston.Logger, app: express.Express): OAuthProvider {
const instance = new OAuthProvider();
try {
OAuthProvider.instance = instance;
instance._logger = logger;
instance.app = app;
instance.oauthServer = new OAuthServer({
model: instance,
grants: ['authorization_code', 'refresh_token'],
accessTokenLifetime: (60 * 60 * 24) * 7, // 7 days * 24 hours, or 1 day
refreshTokenLifetime: (60 * 60 * 24) * 7, // 7 days * 24 hours, or 1 day
allowEmptyState: true,
allowExtendedTokenAttributes: true,
allowBearerTokensInQueryString: true
});
Config.db.query({ _type: "oauthclient" }, null, 10, 0, null, "config", Crypt.rootToken()).then(result => {
instance.clients = result;
}).catch(error => {
instance._logger.error(error);
});
(app as any).oauth = instance.oauthServer;
app.all('/oauth/token', instance.obtainToken.bind(instance));
app.get('/oauth/login', async (req, res) => {
if (NoderedUtil.IsNullUndefinded(instance.clients)) {
instance.clients = await Config.db.query({ _type: "oauthclient" }, null, 10, 0, null, "config", Crypt.rootToken());
if (instance.clients == null || instance.clients.length == 0) return res.status(500).json({ message: 'OAuth not configured' });
}
let state = (req.params.state ? req.params.state : req.params["amp;state"]);
if (state == null) state = encodeURIComponent((req.query.state ? req.query.state : req.query["amp;state"]) as any);
const access_type = (req.query.access_type ? req.query.access_type : req.query["amp;access_type"]);
const client_id = (req.query.client_id ? req.query.client_id : req.query["amp;client_id"]);
const redirect_uri = (req.query.redirect_uri ? req.query.redirect_uri : req.query["amp;redirect_uri"]) as string;
const response_type = (req.query.response_type ? req.query.response_type : req.query["amp;response_type"]);
const scope = (req.query.scope ? req.query.scope : req.query["amp;scope"]);
let client = instance.getClientById(client_id);
if (NoderedUtil.IsNullUndefinded(client)) {
instance.clients = await Config.db.query({ _type: "oauthclient" }, null, 10, 0, null, "config", Crypt.rootToken());
if (instance.clients == null || instance.clients.length == 0) return res.status(500).json({ message: 'OAuth not configured' });
}
if (req.user) {
if (!NoderedUtil.IsNullUndefinded(client) && !Array.isArray(client.redirectUris)) {
client.redirectUris = [];
}
if (!NoderedUtil.IsNullUndefinded(client) && client.redirectUris.length > 0) {
if (client.redirectUris.indexOf(redirect_uri) == -1) {
return res.status(500).json({ message: 'illegal redirect_uri ' + redirect_uri });
// client.redirectUris.push(redirect_uri);
}
}
const code = Math.random().toString(36).substr(2, 9);
instance._logger.info("[OAuth][" + (req.user as any).username + "] /oauth/login " + state);
instance.saveAuthorizationCode(code, client, req.user, redirect_uri);
res.redirect(`${redirect_uri}?state=${state}&code=${code}`);
} else {
instance._logger.info("[OAuth][anon] /oauth/login " + state);
res.cookie("originalUrl", req.originalUrl, { maxAge: 900000, httpOnly: true });
res.redirect("/login");
}
});
// app.get('/oauth/authorize', instance.authorize.bind(instance));
app.all('/oauth/authorize', (req, res) => {
const request = new Request(req);
const response = new Response(res);
return instance.oauthServer.authenticate(request, response)
.then((token) => {
res.json(token.user);
}).catch((err) => {
console.error(err);
res.status(err.code || 500).json(err);
});
});
// app.all('/oauth/authorize', instance.oauthServer.authenticate.bind(instance));
// app.all('/oauth/authorize/emails', instance.oauthServer.authenticate.bind(instance));
} catch (error) {
console.error(error);
const json = JSON.stringify(error, null, 3);
console.error(json);
throw error;
}
return instance;
}
authorize(req, res) {
this._logger.info("[OAuth] authorize");
const request = new Request(req);
const response = new Response(res);
console.log(request.headers);
return this.oauthServer.authorize(request, response)
.then((token) => {
res.json(token);
}).catch((err) => {
console.error(err);
res.status(err.code || 500).json(err);
});
}
authenticateHandler() {
return {
handle: (request, response) => {
//in this example, we store the logged-in user as the 'loginUser' attribute in session
if (request.session.loginUser) {
return { username: request.session.loginUser.username };
}
return null;
}
};
}
obtainToken(req, res) {
this._logger.info("[OAuth] obtainToken");
const request = new Request(req);
const response = new Response(res);
return this.oauthServer.token(request, response)
.then((token) => {
this._logger.info("[OAuth] obtainToken::success: token:");
res.json(token);
}).catch((err) => {
this._logger.info("[OAuth] obtainToken::failed: token:");
console.error(err);
res.status(err.code || 500).json(err);
});
}
public async getAccessToken(accessToken) {
this._logger.info("[OAuth] getAccessToken " + accessToken);
let token = await OAuthProvider.getCachedAccessToken(accessToken);
if (token != null) return token;
const tokens = await Config.db.query({ _type: "token", "accessToken": accessToken }, null, 10, 0, null, "oauthtokens", Crypt.rootToken());
token = tokens.length ? tokens[0] as any : null;
await OAuthProvider.addToken(token);
if (token == null) return false;
return token;
}
public async getRefreshToken(refreshToken) {
this._logger.info("[OAuth] getRefreshToken " + refreshToken);
let token = await OAuthProvider.getCachedAccessToken(refreshToken);
if (token != null) return token;
const tokens = await Config.db.query({ _type: "token", "refreshToken": refreshToken }, null, 10, 0, null, "oauthtokens", Crypt.rootToken());
token = tokens.length ? tokens[0] as any : null;
await OAuthProvider.addToken(token);
if (token == null) return false;
return token;
}
public getClient(clientId, clientSecret) {
this._logger.info("[OAuth] getClient " + clientId);
const clients = this.clients.filter((client) => {
return client.clientId === clientId && client.clientSecret === clientSecret;
});
return clients.length ? clients[0] : false;
}
public getClientById(clientId) {
this._logger.info("[OAuth] getClientById " + clientId);
const clients = this.clients.filter((client) => {
return client.clientId === clientId;
});
return clients.length ? clients[0] : null;
}
public async saveToken(token, client, user) {
this._logger.info("[OAuth] saveToken for " + user.name + " in " + client.clientId);
const result: any = {
name: "Token for " + user.name,
accessToken: token.accessToken,
access_token: token.accessToken,
accessTokenExpiresAt: token.accessTokenExpiresAt,
clientId: client.clientId,
refreshToken: token.refreshToken,
refresh_token: token.refreshToken,
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
userId: user.id,
user: user,
client: client,
_type: "token"
};
await OAuthProvider.addToken(result);
await Config.db.InsertOne(result, "oauthtokens", 0, false, Crypt.rootToken());
return result;
}
public async saveAuthorizationCode(code: string, client: any, user: any, redirect_uri: string) {
this._logger.info("[OAuth] saveAuthorizationCode " + code);
const codeobject = Object.assign({}, user);
delete codeobject._id;
codeobject._type = 'code';
codeobject.code = code;
codeobject.id = user._id;
codeobject.username = user.username;
codeobject.email = user.email;
if (NoderedUtil.IsNullEmpty(codeobject.email)) codeobject.email = user.username;
codeobject.fullname = user.name;
codeobject.redirect_uri = redirect_uri;
codeobject.client_id = client.clientId
codeobject.name = "Code " + code + " for " + user.name
this.codes[code] = codeobject;
await Config.db.InsertOne(codeobject, "oauthtokens", 1, false, Crypt.rootToken());
this._logger.info("[OAuth] saveAuthorizationCode " + code + " saved");
// instance.codes[code].client_id = client_id;
// await Config.db.InsertOne(result, "oauthtokens", 0, false, Crypt.rootToken());
// // const codeToSave: any = this.codes[code];
// const codeToSave: any = {
// 'authorizationCode': code.authorizationCode,
// 'expiresAt': code.expiresAt,
// 'redirectUri': code.redirectUri,
// 'scope': code.scope,
// 'client': client.id,
// 'user': user.username
// };
// this.codes[code] = codeToSave;
// this.revokeAuthorizationCode(code);
// code = Object.assign({}, code, {
// 'client': client.id,
// 'user': user.username
// });
return codeobject;
}
sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
public async getAuthorizationCode(code) {
this._logger.info("[OAuth] getAuthorizationCode " + code);
let user: any = this.codes[code];
if (user == null) {
let users = await Config.db.query({ _type: "code", "code": code }, null, 10, 0, null, "oauthtokens", Crypt.rootToken());
user = users.length ? users[0] as any : null;
if (user == null) {
await this.sleep(1000);
users = await Config.db.query({ _type: "code", "code": code }, null, 10, 0, null, "oauthtokens", Crypt.rootToken());
user = users.length ? users[0] as any : null;
}
if (user == null) {
await this.sleep(1000);
users = await Config.db.query({ _type: "code", "code": code }, null, 10, 0, null, "oauthtokens", Crypt.rootToken());
user = users.length ? users[0] as any : null;
}
if (user == null) {
this._logger.error("[OAuth] getAuthorizationCode, unkown code '" + code + "'");
return null;
}
if (user != null) { this.codes[code] = user; }
}
const client_id: string = this.codes[code].client_id;
if (user == null) return null;
this.revokeAuthorizationCode(code);
const redirect_uri = (user as any).redirect_uri;
const expiresAt = new Date((new Date).getTime() + (1000 * Config.oauth_access_token_lifetime));
var tuser = TokenUser.From(user);
let client = this.getClientById(client_id);
if (NoderedUtil.IsNullUndefinded(client)) return null;
let role = client.defaultrole;
const keys: string[] = Object.keys(client.rolemappings);
for (let i = 0; i < keys.length; i++) {
if (tuser.HasRoleName(keys[i])) role = client.rolemappings[keys[i]];
}
const result = {
code: code,
client: this.clients[0],
user: {
id: user.id,
_id: user.id,
name: user.fullname,
username: user.username,
email: user.username,
role: role
},
expiresAt: expiresAt,
redirectUri: redirect_uri
}
// node-bb username hack
if (result.user.name == result.user.email && result.user.email.indexOf("@") > -1) {
result.user.name = result.user.email.substr(0, result.user.email.indexOf("@") - 1);
}
if (result.user.name == result.user.email && result.user.email.indexOf("@") == -1) {
result.user.email = result.user.email + "@unknown.local"
}
if (result.user.name == result.user.email) {
result.user.name = "user " + result.user.email;
}
return result;
}
public async revokeAuthorizationCode(code) {
if (typeof code !== "string") { code = code.code; }
this._logger.info("[OAuth] revokeAuthorizationCode " + code);
delete this.codes[code];
const refreshTokenExpiresAt = new Date((new Date).getTime() - (1000 * Config.oauth_refresh_token_lifetime)).toISOString();
const accessTokenExpiresAt = new Date((new Date).getTime() - (1000 * Config.oauth_access_token_lifetime)).toISOString();
const codeExpiresAt = new Date((new Date).getTime() - (1000 * 120)).toISOString();
await Config.db.DeleteMany({
"$or":
[
{ "_type": "code", "_created": { "$lte": codeExpiresAt } },
{ "_type": "token", "refreshTokenExpiresAt": { "$lte": refreshTokenExpiresAt } },
{ "_type": "token", "accessTokenExpiresAt": { "$lte": accessTokenExpiresAt } }
]
}, null, "oauthtokens", Crypt.rootToken());
// await Config.db.DeleteMany({ "_type": "code", "code": code }, null, "oauthtokens", Crypt.rootToken());
await Config.db.DeleteMany({ "_type": "code", "_created": { "$lte": codeExpiresAt } }, null, "oauthtokens", Crypt.rootToken());
return true;
// const user: TokenUser = this.codes[code];
// if (user != null) delete this.codes[code];
// return code;
}
public static tokenCache: CachedToken[] = [];
private static async getCachedAccessToken(accessToken: string): Promise {
await semaphore.down();
for (let i = this.tokenCache.length - 1; i >= 0; i--) {
var res: CachedToken = this.tokenCache[i];
var begin: number = res.firstsignin.getTime();
var end: number = new Date().getTime();
var seconds = Math.round((end - begin) / 1000);
if (seconds > Config.oauth_token_cache_seconds) {
this.tokenCache.splice(i, 1);
} else if (res.token.accessToken == accessToken) {
// console.log("Return token from cache, using accessToken " + accessToken);
semaphore.up();
return res.token;
}
}
semaphore.up();
return null;
}
private static async getCachedRefreshToken(refreshToken: string): Promise {
await semaphore.down();
for (let i = this.tokenCache.length - 1; i >= 0; i--) {
var res: CachedToken = this.tokenCache[i];
var begin: number = res.firstsignin.getTime();
var end: number = new Date().getTime();
var seconds = Math.round((end - begin) / 1000);
if (seconds > Config.oauth_token_cache_seconds) {
this.tokenCache.splice(i, 1);
} else if (res.token.refreshToken == refreshToken) {
// console.log("Return token from cache, using refreshToken " + refreshToken);
semaphore.up();
return res.token;
}
}
semaphore.up();
return null;
}
private static async addToken(token: any) {
await semaphore.down();
// console.log("Adding token to cache");
var cuser: CachedToken = new CachedToken(token);
this.tokenCache.push(cuser);
semaphore.up();
}
}
export class CachedToken {
public firstsignin: Date;
constructor(
public token: any
) {
this.firstsignin = new Date();
}
}
interface HashTable {
[key: string]: T;
}
const Semaphore = (n) => ({
n,
async down() {
while (this.n <= 0) await this.wait();
this.n--;
},
up() {
this.n++;
},
async wait() {
if (this.n <= 0) return await new Promise((res, req) => {
setImmediate(async () => res(await this.wait()))
});
return;
},
});
const semaphore = Semaphore(1);