Skip to content

Commit

Permalink
More TS refactoring of the core func
Browse files Browse the repository at this point in the history
  • Loading branch information
hrax committed Jul 27, 2024
1 parent 4f0bb31 commit be4dbc4
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 163 deletions.
27 changes: 27 additions & 0 deletions modules/@types/instance-extended.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface SNOAuthToken {
access_token: string;
refresh_token: string;
scope: string;
token_type: string;
expires_in: number;
}

export interface InstanceOAuthData {
lastRetrieved: number;
token?: SNOAuthToken | null;
}

export interface InstanceAuthenticationData extends InstanceOAuthData {
type: "oauth-token" | "oauth-password";
clientID: string;
clientSecret: string;
}

export interface InstanceConnectionData {
baseUrl: string;
}

export interface InstanceConfig extends InstanceConnectionData {
name: string;
auth: InstanceAuthenticationData;
}
49 changes: 30 additions & 19 deletions modules/core/InstanceManager.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { InstanceConfig } from "../@types/instance-extended";

export type InstanceData = {
name: string,
base: string,
auth: {
type: "oauth-token" | "oauth-password",
clientID: string,
clientSecret: string,
token?: {
"access_token": string,
"refresh_token": string,
"scope": string,
"token_type": string,
"expires_in": number,
"loaded_at": Date
export default class InstanceManager {
private instance: InstanceConfig = {
name: "",
baseUrl: "",
auth: {
type: "oauth-token",
clientID: "",
clientSecret: "",
lastRetrieved: 0,
token: {
access_token: "",
refresh_token: "",
scope: "",
token_type: "",
expires_in: 0
}
}
};

static load(path: string): InstanceManager | null {
return null;
}
}

export default class InstanceManager {
#instance: InstanceData;
static save(instance: InstanceManager): void {

}

constructor(instance: InstanceConfig) {
this.setInstanceData(instance);
}

constructor(instance: InstanceData) {
this.#instance = instance;
setInstanceData(instance: InstanceConfig): void {
this.instance = instance;
}
}
142 changes: 108 additions & 34 deletions modules/core/OAuthClient.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,62 @@
import crypto from "crypto";
import { URL, URLSearchParams } from "url";
import { InstanceData } from "./InstanceManager";
import Request, { RESPONSE_STATUS } from "./Request";
import Request, { ResponseStatus, ResponseError, Response } from "./Request";
import pkg from "../../package.json";
import { RequestOptions } from "https";
import { IncomingMessage } from "http";
import { InstanceAuthenticationData, InstanceConfig, InstanceOAuthData, SNOAuthToken } from "../@types/instance-extended";

const CLIENT_BASE_URL: string = "/oauth_entity.do";
const CLIENT_LIST_BASE_URL: string = "/oauth_entity.do";
const CLIENT_LIST_BASE_URL: string = "/oauth_entity_list.do";
const AUTH_BASE_URL: string = "/oauth_auth.do";
const TOKEN_BASE_URL: string = "/oauth_token.do";

// TODO: Allow to configure server port via ENV, just warn that it should be in redirects!
const REDIRECT_URI: string = "http://localhost:696969/oauth_client";

const TOKEN_CONTENT_TYPE: string = "application/x-www-form-urlencoded"

export type OAuthTokenResponse = {
"access_token": string,
"refresh_token": string,
"scope": string,
"token_type": string,
"expires_in": number
export class OAuthRefreshTokenExpired extends Error {
constructor (message?: string, options?: ErrorOptions) {
super(message, options);
}
};
export class OAuthTokenExpired extends Error {
constructor (message?: string, options?: ErrorOptions) {
super(message, options);
}
};
export class OAuthCodeExpired extends Error {
constructor (message?: string, options?: ErrorOptions) {
super(message, options);
}
};
export class OAuthUsernamePasswordIncorrect extends Error {
constructor (message?: string, options?: ErrorOptions) {
super(message, options);
}
};

export default class OAuthClient {
#instance: InstanceData;
private instance: InstanceConfig;

// create Server?
// create Server for code?

static generateRandomState(): string {
return crypto.randomBytes(16).toString("hex");
}

constructor(instance: InstanceData) {
this.#instance = instance;
constructor(instance: InstanceConfig) {
this.instance = instance;
}

private setToken(token: SNOAuthToken) {
this.instance.auth.lastRetrieved = Date.now();
this.instance.auth.token = token;
}

getInstanceConfig(): InstanceConfig {
return this.instance;
}

getNewClientURL(): string {
Expand All @@ -45,7 +69,7 @@ export default class OAuthClient {
"logo_url="
];

const url: URL = new URL(CLIENT_BASE_URL, this.#instance.base);
const url: URL = new URL(CLIENT_BASE_URL, this.instance.baseUrl);
url.searchParams.set("sys_id", "-1");
url.searchParams.set("sysparm_transaction_scope", "global");
url.searchParams.set("sysparm_query", query.join("^"));
Expand All @@ -57,31 +81,42 @@ export default class OAuthClient {
"type=client",
"name=" + pkg.name
];
const url: URL = new URL(CLIENT_LIST_BASE_URL, this.#instance.base);
const url: URL = new URL(CLIENT_LIST_BASE_URL, this.instance.baseUrl);
url.searchParams.set("sys_id", "-1");
url.searchParams.set("sysparm_transaction_scope", "global");
url.searchParams.set("sysparm_query", query.join("^"));
return url.toString();
}

getAuthCodeURL(state: string): string {
const url: URL = new URL(AUTH_BASE_URL, this.#instance.base);
const url: URL = new URL(AUTH_BASE_URL, this.instance.baseUrl);
url.searchParams.set("response_type", "code");
url.searchParams.set("redirect_uri", REDIRECT_URI);
url.searchParams.set("client_id", this.#instance.auth.clientID);
url.searchParams.set("client_id", this.instance.auth.clientID);
url.searchParams.set("state", state);
return url.toString();
}

async requestTokenByCode(authCode: string): Promise<OAuthTokenResponse> {
const url: URL = new URL(TOKEN_BASE_URL, this.#instance.base);
isTokenExpired(): boolean {
const oauth: InstanceOAuthData = this.instance.auth;
if (oauth.token == null) {
return true;
}
const hadTokenFor = Date.now() - oauth.lastRetrieved + 10000;
const expiresIn = oauth.token.expires_in * 1000;
return hadTokenFor < expiresIn;
}

async requestTokenByCode(code: string): Promise<SNOAuthToken> {
const oauth: InstanceAuthenticationData = this.instance.auth;
const url: URL = new URL(TOKEN_BASE_URL, this.instance.baseUrl);

const body: URLSearchParams = new URLSearchParams();
body.set("grant_type", "authorization_code");
body.set("code", authCode);
body.set("code", code);
//body.set("redirect_uri", REDIRECT_URI);
body.set("client_id", this.#instance.auth.clientID);
body.set("client_secret", this.#instance.auth.clientSecret);
body.set("client_id", oauth.clientID);
body.set("client_secret", oauth.clientSecret);

const options: RequestOptions = {
method: "POST",
Expand All @@ -91,18 +126,31 @@ export default class OAuthClient {
}
};

return await Request.json(url, options, body.toString());
const token: SNOAuthToken = await Request.json<SNOAuthToken>(url, options, body.toString())
.catch<SNOAuthToken>((reason: any) => {
if (reason instanceof ResponseError) {
const response: IncomingMessage | null = (<ResponseError>reason).response.http;
if (response != null && response.statusCode == ResponseStatus.UNAUTHORIZED) {
throw new OAuthCodeExpired(<any>reason);
}
}
throw Error(reason);
});

this.setToken(token);
return token;
}

async requestTokenByUsername(username: string, password: string): Promise<OAuthTokenResponse> {
const url: URL = new URL(TOKEN_BASE_URL, this.#instance.base);
async requestTokenByUsername(username: string, password: string): Promise<SNOAuthToken> {
const oauth: InstanceAuthenticationData = this.instance.auth;
const url: URL = new URL(TOKEN_BASE_URL, this.instance.baseUrl);

const body: URLSearchParams = new URLSearchParams();
body.set("grant_type", "password");
body.set("username", username);
body.set("password", password);
body.set("client_id", this.#instance.auth.clientID);
body.set("client_secret", this.#instance.auth.clientSecret);
body.set("client_id", oauth.clientID);
body.set("client_secret", oauth.clientSecret);

const options: RequestOptions = {
method: "POST",
Expand All @@ -112,17 +160,31 @@ export default class OAuthClient {
}
};

return await Request.json(url, options, body.toString());
const token: SNOAuthToken = await Request.json<SNOAuthToken>(url, options, body.toString())
.catch<SNOAuthToken>((reason: any) => {
if (reason instanceof ResponseError) {
// 401: username/password incorrect
const response: IncomingMessage | null = (<ResponseError>reason).response.http;
if (response != null && response.statusCode == ResponseStatus.UNAUTHORIZED) {
throw new OAuthUsernamePasswordIncorrect(<any>reason);
}
}
throw Error(reason);
});

this.setToken(token);
return token;
}

async refreshToken(): Promise<OAuthTokenResponse> {
const url: URL = new URL(TOKEN_BASE_URL, this.#instance.base);
async refreshToken(): Promise<SNOAuthToken> {
const oauth: InstanceAuthenticationData = this.instance.auth;
const url: URL = new URL(TOKEN_BASE_URL, this.instance.baseUrl);

const body: URLSearchParams = new URLSearchParams();
body.set("grant_type", "refresh_token");
body.set("refresh_token", this.#instance.auth.token?.refresh_token!);
body.set("client_id", this.#instance.auth.clientID);
body.set("client_secret", this.#instance.auth.clientSecret);
body.set("refresh_token", oauth.token!.refresh_token);
body.set("client_id", oauth.clientID);
body.set("client_secret", oauth.clientSecret);

const options: RequestOptions = {
method: "POST",
Expand All @@ -132,6 +194,18 @@ export default class OAuthClient {
}
};

return await Request.json(url, options, body.toString());
const token: SNOAuthToken = await Request.json<SNOAuthToken>(url, options, body.toString())
.catch<SNOAuthToken>((reason: any) => {
if (reason instanceof ResponseError) {
const response: Response = (<ResponseError>reason).response;
if (Response.isUnauthorized(response)) {
throw new OAuthRefreshTokenExpired(<any>reason);
}
}
throw Error(reason);
});

this.setToken(token);
return token;
}
}
Loading

0 comments on commit be4dbc4

Please sign in to comment.