automatic refresh tokens
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@ -31,6 +31,9 @@ interface AccessResponse {
|
||||
refresh_expires_in?: number;
|
||||
}
|
||||
|
||||
type AuthHandler =
|
||||
(req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
||||
|
||||
class AuthRequest {
|
||||
public request: IncomingMessage;
|
||||
public response: ServerResponse;
|
||||
@ -47,6 +50,7 @@ class AuthRequest {
|
||||
public body: string;
|
||||
|
||||
private _cookie_name?: string;
|
||||
private _refresh_cookie_name?: string;
|
||||
private _is_successful: boolean;
|
||||
|
||||
public get is_successful (): boolean {
|
||||
@ -57,7 +61,8 @@ class AuthRequest {
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
body: string,
|
||||
cookie?: string
|
||||
cookie?: string,
|
||||
refresh_cookie?: string
|
||||
) {
|
||||
this.request = req;
|
||||
this.response = res;
|
||||
@ -67,6 +72,7 @@ class AuthRequest {
|
||||
this.user = '';
|
||||
this.password = '';
|
||||
this._cookie_name = cookie;
|
||||
this._refresh_cookie_name = refresh_cookie;
|
||||
this._is_successful = false;
|
||||
}
|
||||
|
||||
@ -102,12 +108,10 @@ class AuthRequest {
|
||||
expires_in: access_token_expires_in
|
||||
};
|
||||
|
||||
if (typeof this._cookie_name === 'string') {
|
||||
this.response.setHeader (
|
||||
'Set-Cookie',
|
||||
`${this._cookie_name}=${at.signature}`
|
||||
);
|
||||
}
|
||||
const cookies = [];
|
||||
|
||||
if (typeof this._cookie_name === 'string')
|
||||
cookies.push (`${this._cookie_name}=${at.signature}`);
|
||||
|
||||
if (include_refresh_token) {
|
||||
if (typeof refresh_token_expires_in !== 'number')
|
||||
@ -120,6 +124,16 @@ class AuthRequest {
|
||||
res.refresh_token = rt.signature;
|
||||
res.refresh_expires_in = refresh_token_expires_in;
|
||||
result.refresh_token_id = rt.id;
|
||||
|
||||
if (typeof this._refresh_cookie_name === 'string')
|
||||
cookies.push (`${this._refresh_cookie_name}=${rt.signature}`);
|
||||
}
|
||||
|
||||
if (cookies.length > 0) {
|
||||
this.response.setHeader (
|
||||
'Set-Cookie',
|
||||
cookies
|
||||
);
|
||||
}
|
||||
|
||||
this._is_successful = true;
|
||||
@ -194,6 +208,7 @@ interface CreateHandlerOptions {
|
||||
refresh?: AccessSettings;
|
||||
modules?: Record<string, AuthRequestHandler>;
|
||||
cookie_name?: string;
|
||||
refresh_cookie_name?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
@ -261,7 +276,13 @@ function process_request (
|
||||
export default function create_auth_handler (
|
||||
default_handler: AuthRequestHandler,
|
||||
options?: CreateHandlerOptions
|
||||
) {
|
||||
): AuthHandler {
|
||||
if (
|
||||
typeof options?.cookie_name !== 'undefined'
|
||||
&& options.cookie_name === options.refresh_cookie_name
|
||||
)
|
||||
throw new Error ('access and refresh cookies cannot have the same name');
|
||||
|
||||
return async (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse
|
||||
@ -276,7 +297,13 @@ export default function create_auth_handler (
|
||||
});
|
||||
});
|
||||
|
||||
const request = new AuthRequest (req, res, body, options?.cookie_name);
|
||||
const request = new AuthRequest (
|
||||
req,
|
||||
res,
|
||||
body,
|
||||
options?.cookie_name,
|
||||
options?.refresh_cookie_name
|
||||
);
|
||||
const token = (/(?<type>\S+) (?<token>.+)/ui)
|
||||
.exec (req.headers.authorization as string);
|
||||
|
||||
@ -292,5 +319,6 @@ export {
|
||||
AccessResponse,
|
||||
AuthRequest,
|
||||
AuthRequestHandler,
|
||||
CreateHandlerOptions
|
||||
CreateHandlerOptions,
|
||||
AuthHandler
|
||||
};
|
||||
|
@ -8,6 +8,7 @@
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
import { run_regex } from '@sapphirecode/utilities';
|
||||
import authority from './Authority';
|
||||
import { AuthRequest, AccessSettings } from './AuthHandler';
|
||||
|
||||
type AnyFunc = (...args: unknown[]) => unknown;
|
||||
type Gateway = (
|
||||
@ -15,15 +16,33 @@ type Gateway = (
|
||||
res: ServerResponse, next: AnyFunc
|
||||
) => unknown;
|
||||
|
||||
interface RefreshSettings extends AccessSettings {
|
||||
leave_open?: never;
|
||||
redirect_to?: never;
|
||||
}
|
||||
|
||||
interface GatewayOptions {
|
||||
redirect_url?: string;
|
||||
cookie_name?: string;
|
||||
refresh_cookie_name?: string;
|
||||
refresh_settings?: RefreshSettings;
|
||||
}
|
||||
|
||||
interface AuthCookies {
|
||||
access_cookie: string | null;
|
||||
refresh_cookie: string | null;
|
||||
}
|
||||
|
||||
class GatewayClass {
|
||||
private _options: GatewayOptions;
|
||||
|
||||
public constructor (options: GatewayOptions = {}) {
|
||||
if (
|
||||
typeof options.cookie_name === 'string'
|
||||
&& options.cookie_name === options.refresh_cookie_name
|
||||
)
|
||||
throw new Error ('access and refresh cookies cannot have the same name');
|
||||
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
@ -52,25 +71,33 @@ class GatewayClass {
|
||||
return auth.groups?.data;
|
||||
}
|
||||
|
||||
public get_cookie_auth (req: IncomingMessage): string | null {
|
||||
if (typeof this._options.cookie_name === 'undefined')
|
||||
return null;
|
||||
let auth = null;
|
||||
public get_cookie_auth (req: IncomingMessage): AuthCookies {
|
||||
const result: AuthCookies = {
|
||||
access_cookie: null,
|
||||
refresh_cookie: null
|
||||
};
|
||||
|
||||
const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu;
|
||||
|
||||
run_regex (
|
||||
/(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu,
|
||||
cookie_regex,
|
||||
req.headers.cookie,
|
||||
(res: RegExpMatchArray) => {
|
||||
if (res.groups?.name === this._options.cookie_name)
|
||||
auth = res.groups?.value;
|
||||
result.access_cookie = res.groups?.value as string;
|
||||
else if (res.groups?.name === this._options.refresh_cookie_name)
|
||||
result.refresh_cookie = res.groups?.value as string;
|
||||
}
|
||||
);
|
||||
return auth;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public authenticate (req: IncomingMessage): boolean {
|
||||
const cookies = this.get_cookie_auth (req);
|
||||
let auth = this.get_header_auth (req);
|
||||
if (auth === null)
|
||||
auth = this.get_cookie_auth (req);
|
||||
auth = cookies.access_cookie;
|
||||
if (auth === null)
|
||||
return false;
|
||||
|
||||
@ -82,13 +109,55 @@ class GatewayClass {
|
||||
return ver.authorized;
|
||||
}
|
||||
|
||||
public process_request (
|
||||
public async try_refresh (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
typeof this._options.refresh_cookie_name === 'undefined'
|
||||
|| typeof this._options.refresh_settings === 'undefined'
|
||||
)
|
||||
return false;
|
||||
|
||||
const refresh = this.get_cookie_auth (req).refresh_cookie;
|
||||
if (refresh === null)
|
||||
return false;
|
||||
|
||||
const ver = authority.verify (refresh);
|
||||
if (ver.type === 'refresh_token' && ver.valid) {
|
||||
const auth_request = new AuthRequest (
|
||||
req,
|
||||
res,
|
||||
''
|
||||
, this._options.cookie_name,
|
||||
this._options.refresh_cookie_name
|
||||
);
|
||||
const refresh_result = await auth_request.allow_access ({
|
||||
...this._options.refresh_settings,
|
||||
leave_open: true
|
||||
});
|
||||
|
||||
const con = req.connection as unknown as Record<string, unknown>;
|
||||
con.auth = {
|
||||
token_id: refresh_result.access_token_id,
|
||||
token_data: this._options.refresh_settings.data
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async process_request (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: AnyFunc
|
||||
): unknown {
|
||||
): Promise<unknown> {
|
||||
if (this.authenticate (req))
|
||||
return next ();
|
||||
if (await this.try_refresh (req, res))
|
||||
return next ();
|
||||
return this.redirect (res);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ import create_auth_handler, {
|
||||
AuthRequestHandler,
|
||||
AuthRequest,
|
||||
AccessSettings,
|
||||
AccessResult
|
||||
AccessResult,
|
||||
AuthHandler
|
||||
} from './AuthHandler';
|
||||
import authority, {
|
||||
VerificationResult,
|
||||
@ -44,6 +45,7 @@ export {
|
||||
CreateHandlerOptions,
|
||||
AuthRequestHandler,
|
||||
AuthRequest,
|
||||
AuthHandler,
|
||||
AccessSettings,
|
||||
AccessResult,
|
||||
VerificationResult,
|
||||
|
Reference in New Issue
Block a user