@humandialog/auth.svelte
Version:
Svelte package to deal with ObjectReef OAuth 2 Identity Provider
823 lines (672 loc) • 26.2 kB
text/typescript
import { Token } from "./Token"
import { gv, type Browser_storage } from "./Storage";
import { Configuration, Mode, Local_user } from "./Configuration";
import {Writable, writable} from 'svelte/store'
export class User
{
public given_name :string = "";
public family_name :string = "";
public picture :string = "";
public email :string = "";
public email_verified :boolean = false;
}
export class Header_info
{
public key: string
public value: string
}
export class Tenant_info
{
public id: string
public url: string
public name: string = ""
public headers: Header_info[] | undefined
}
export class App_instance_info
{
public tenant_id: string = ""
public name: string = ""
public desc: string = ""
public img: string = ""
public is_public: boolean = false;
public unauthorized_guest_allowed: boolean = false;
}
export class Session
{
private my_validation_ticket :number = 0;
private _is_active :boolean = false;
private _user :User;
private _id_token :Token;
private _access_token :Token;
private _refresh_token :Token;
private storage :Browser_storage;
public appInstanceInfo :App_instance_info|null = null;
public configuration :Configuration;
public sessionId :string
constructor(storage: Browser_storage)
{
this.storage = storage;
let arr = new Uint8Array((16) / 2)
window.crypto.getRandomValues(arr)
const dec2hex = (dec)=> dec.toString(16).padStart(2, "0")
this.sessionId = Array.from(arr, dec2hex).join('')
}
public configure(cfg, internal=false)
{
console.log('configure', 'internal', internal, cfg)
this.configuration = new Configuration;
if(cfg)
{
switch(cfg.mode)
{
case 'remote':
this.configuration.mode = Mode.Remote;
this.configuration.iss = cfg.remote.iss;
this.configuration.client_id = cfg.remote.client_id ?? cfg.remote.clientID;
this.configuration.client_secret = cfg.remote.client_secret ?? cfg.remote.clientSecret;
this.configuration.scope = cfg.remote.scope;
this.configuration.api_version = cfg.remote.api_version ?? cfg.remote.apiVersion ?? "v001";
this.configuration.tenant = cfg.remote.tenant ?? "";
this.configuration.groups_only = cfg.remote.groupsOnly ?? cfg.remote.groups_only ?? false;
this.configuration.ask_organization_name = cfg.remote.ask_organization_name ?? cfg.remote.askOrganizationName ?? true
this.configuration.refresh_token_persistent = cfg.remote.refresh_token_persistent ?? cfg.remote.refreshTokenPersistent ?? true;
this.configuration.terms_and_conditions_href = cfg.remote.terms_and_conditions_href ?? cfg.remote.termsAndConditionsHRef;
this.configuration.privacy_policy_href = cfg.remote.privacy_policy_href ?? cfg.remote.privacyPolicyHRef;
this.configuration.let_choose_group_first = cfg.remote.let_choose_group_first ?? cfg.remote.letChooseGroupFirst ?? false;
break;
case 'local':
this.configuration.mode = Mode.Local;
this.configuration.local_api = cfg.local.api;
this.configuration.api_version = cfg.local.api_version ?? cfg.local.apiVersion ?? "v001";
this.configuration.local_users = [];
if(cfg.local.users && Array.isArray(cfg.local.users))
{
cfg.local.users.forEach(u => {
switch(typeof u)
{
case 'string':
{
const user = new Local_user();
user.username = u;
this.configuration.local_users.push(user);
}
break;
case 'object':
{
const user = new Local_user();
user.username = u.username ?? "";
user.role = u.role ?? "";
user.groupId = u.groupId ?? 0;
user.uid = u.uid ?? 0;
this.configuration.local_users.push(user);
}
break;
}
});
}
break;
case 'disabled':
this.configuration.mode = Mode.Disabled;
this.configuration.local_api = cfg.local.api;
this.configuration.api_version = cfg.local.api_version ?? cfg.local.apiVersion ?? "v001";
break;
}
}
else
{
this.configuration.mode = Mode.Disabled;
}
this.setup_mode(this.configuration.mode);
if(!internal)
{
this.storage.set('_hd_auth_cfg', JSON.stringify(cfg))
if(this.isValid)
this.boost_validation_ticket();
let new_session :Session = new Session(this.storage);
new_session.clone_from(this);
session.set(new_session); // forces store subscribers
}
}
private clone_from(src :Session)
{
this.my_validation_ticket = src.my_validation_ticket;
this._is_active = src._is_active;
this._user = src._user;
this._id_token = src._id_token;
this._access_token = src._access_token;
this._refresh_token = src._refresh_token;
this.configuration = src.configuration;
}
public get isActive() :boolean
{
if(!this.isValid)
this.validate();
return this._is_active;
}
public get user() :User
{
if(!this.isValid)
this.validate();
return this._user;
}
public get idToken() :Token
{
if(!this.isValid)
this.validate();
return this._id_token;
}
public get accessToken() :Token
{
if(!this.isValid)
this.validate();
return this._access_token;
}
public get refreshToken() :Token
{
if(!this.isValid)
this.validate();
return this._refresh_token;
}
public get isValid() :boolean
{
let ticket :number;
if(!this.storage.get_num("_hd_auth_session_validation_ticket", (v) => {ticket = v;}))
return false;
return (ticket == this.my_validation_ticket);
}
public get apiAddress() :string
{
let res :string;
if(this.storage.get("_hd_auth_api_address", (v)=>{res=v;}))
return res;
else
return "";
}
public get tid() :string
{
let res :string;
if(this.storage.get("_hd_auth_tenant", (v)=>{res=v;}))
return res;
else
return "";
}
public get appId() :string
{
let scopes = this.configuration.scope.split(' ')
if(!scopes.length)
return '';
//remove predefined scopes
scopes = scopes.filter( s => (s!='openid') && (s!='profile') && (s!='email') && (s!='address') && (s!='phone'));
if(!scopes.length)
return '';
let app_id = scopes[0];
return app_id;
}
public get tenants(): Tenant_info[]
{
let res: string;
if(!this.storage.get("_hd_signedin_tenants", (v)=>{res=v;}))
return [];
if(!res)
return [];
const tenants : Tenant_info[] = JSON.parse(res);
return tenants;
}
public set tenants(infos: Tenant_info[])
{
const tInfos = JSON.stringify(infos)
this.storage.set("_hd_signedin_tenants", tInfos, false);
}
public get isUnauthorizedGuest() :boolean
{
let result: boolean = false;
let res: string;
if(!this.storage.get("_hd_auth_unauthorized_guest", (v)=>{res=v;}))
result = false;
else if(res == "1")
result = true;
else
result = false;
return result;
}
public set isUnauthorizedGuest(val :boolean)
{
this.storage.set("_hd_auth_unauthorized_guest", val ? "1" : "", true);
}
protected validate() : void
{
if(!this.storage.get_num("_hd_auth_session_validation_ticket", (v) => {this.my_validation_ticket = v;}))
{
this.my_validation_ticket = 1;
this.storage.set_num("_hd_auth_session_validation_ticket", this.my_validation_ticket);
}
if(!this.configuration)
{
let cfg_json;
if(this.storage.get('_hd_auth_cfg', (v) => cfg_json = v))
{
try
{
let cfg = JSON.parse(cfg_json);
this.configure(cfg, true);
}
catch(err)
{
console.error(err);
}
}
}
if(this.disabled)
{
this.setCurrentTenantAPI(this.configuration.local_api, '')
this._is_active = true;
return;
}
else if(this.local)
{
if(this.localDevCurrentUser)
{
this._is_active = true;
}
else
{
this._is_active = false;
}
this.setCurrentTenantAPI(this.configuration.local_api, '')
return;
}
this._is_active = false;
let token :string;
if(this.storage.get("_hd_auth_id_token", (v) => {token=v;}))
{
this._id_token = new Token(token);
this._user = new User();
this._user.given_name = this._id_token.get_claim<string>("given_name");
this._user.family_name = this._id_token.get_claim<string>("family_name");
this._user.picture = this._id_token.get_claim<string>("picture");
this._user.email = this._id_token.get_claim<string>("email");
this._user.email_verified = this._id_token.get_claim<boolean>("email_verified");
}
else
this._id_token = null;
if(this.storage.get("_hd_auth_access_token", (v) => {token=v;}))
this._access_token = new Token(token);
else
this._access_token = null;
if(this.storage.get("_hd_auth_refresh_token", (v) => {token=v;}))
this._refresh_token = new Token(token, false);
else
this._refresh_token = null;
if((this._access_token != null) || (this._id_token != null))
this._is_active = true;
}
public refreshTokens(tokens_info, chosen_tenant_id = undefined): boolean
{
if(!tokens_info.access_token)
return false;
if(!tokens_info.id_token)
return false;
if(!tokens_info.refresh_token)
return false;
this.storage.set("_hd_auth_id_token", tokens_info.id_token);
this.storage.set("_hd_auth_access_token", tokens_info.access_token);
this.storage.set("_hd_auth_refresh_token", tokens_info.refresh_token, this.configuration.refresh_token_persistent);
this._id_token = new Token(tokens_info.id_token);
this._user = new User();
this._user.given_name = this._id_token.get_claim<string>("given_name");
this._user.family_name = this._id_token.get_claim<string>("family_name");
this._user.picture = this._id_token.get_claim<string>("picture");
this._user.email = this._id_token.get_claim<string>("email");
this._user.email_verified = this._id_token.get_claim<boolean>("email_verified");
this._access_token = new Token(tokens_info.access_token);
this._refresh_token = new Token(tokens_info.refresh_token, false);
this._is_active = true;
if(tokens_info.tenant != undefined)
{
this.setCurrentTenantAPI(tokens_info.tenant.url, tokens_info.tenant.id);
this.tenants = [tokens_info.tenant];
}
else if((tokens_info.tenants != undefined) && (tokens_info.tenants.length > 0))
{
if(tokens_info.tenants.length == 1)
this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
else
{
if(chosen_tenant_id)
{
const chosen_tenant = tokens_info.tenants.find( el => el.id == chosen_tenant_id)
if(chosen_tenant)
this.setCurrentTenantAPI(chosen_tenant.url, chosen_tenant.id);
else
this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
}
else
this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
}
this.tenants = tokens_info.tenants
}
else
return false;
return true;
}
public signin(tokens_info, chosen_tenant_id = undefined) :boolean
{
if((tokens_info.access_token == undefined) || (tokens_info.access_token == ""))
{
this.signout();
return true;
}
if((tokens_info.id_token == undefined) || (tokens_info.id_token == ""))
{
this.signout();
return true;
}
if((tokens_info.refresh_token == undefined) || (tokens_info.refresh_token == ""))
{
this.signout();
return true;
}
this.storage.set("_hd_auth_access_token", tokens_info.access_token);
this.storage.set("_hd_auth_id_token", tokens_info.id_token);
this.storage.set("_hd_auth_refresh_token", tokens_info.refresh_token, this.configuration.refresh_token_persistent);
if(tokens_info.tenant != undefined)
{
this.setCurrentTenantAPI(tokens_info.tenant.url, tokens_info.tenant.id);
this.tenants = [tokens_info.tenant];
}
else if((tokens_info.tenants != undefined) && (tokens_info.tenants.length > 0))
{
if(tokens_info.tenants.length == 1)
this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
else
{
if(chosen_tenant_id)
{
const chosen_tenant = tokens_info.tenants.find( el => el.id == chosen_tenant_id)
if(chosen_tenant)
this.setCurrentTenantAPI(chosen_tenant.url, chosen_tenant.id);
else
this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
}
else
this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
}
this.tenants = tokens_info.tenants
}
else if((tokens_info.apps != undefined) && (tokens_info.apps.length > 0))
{
// todo: multi app not supported yet?
this.signout();
return false;
}
else
{
this.signout();
return false;
}
this.boost_validation_ticket();
this.validate();
this.checkServerAndClientTimeMismatch();
let new_session :Session = new Session(this.storage);
new_session.clone_from(this);
session.set(new_session); // forces store subscribers
return true;
}
protected checkServerAndClientTimeMismatch() :void
{
if(!this._access_token)
return;
const serverTime = this._access_token.get_claim<number>("iat");
if(!serverTime)
return;
const clientTime = Math.floor(Date.now() / 1000);
const timeShift = clientTime - serverTime;
// now just logging. In the near future we need to store this value and use on Token::not_expired property
console.log('Server/Client time mismatch: ', timeShift);
}
protected boost_validation_ticket() :void
{
let validation_ticket :number = 0;
this.storage.get_num("_hd_auth_session_validation_ticket", (v) => {validation_ticket = v;});
validation_ticket++;
this.storage.set_num("_hd_auth_session_validation_ticket", validation_ticket);
this.my_validation_ticket = validation_ticket;
}
public setCurrentTenantAPI(url :string, tid :string) :void
{
this.storage.set("_hd_auth_api_address", url);
this.storage.set("_hd_auth_tenant", tid);
this.storage.set('_hd_auth_last_chosen_tenant_id', tid, true);
}
public get lastChosenTenantId() :string
{
let res;
if(!this.storage.get('_hd_auth_last_chosen_tenant_id', (v) => res=v))
return '';
return res;
}
public signout() : void
{
this.storage.set("_hd_auth_id_token", "");
this.storage.set("_hd_auth_access_token", "");
this.storage.set("_hd_auth_refresh_token", "", this.configuration.refresh_token_persistent);
this.storage.set("_hd_auth_api_address", "");
this.storage.set("_hd_auth_tenant", "");
this.storage.set("_hd_auth_local_dev_user", "");
this.storage.set('_hd_auth_unauthorized_guest', "", true)
this._id_token = null;
this._access_token = null;
this._refresh_token = null;
this._is_active = false;
this.boost_validation_ticket();
let new_session :Session = new Session(this.storage);
new_session.clone_from(this);
session.set(new_session); // forces store subscribers
}
public appAccessRole() : string
{
if(!this.configuration)
return '';
const scopes = this.configuration.scope.split(' ')
if((!scopes) || scopes.length == 0)
return '';
const appId = scopes[scopes.length-1];
if(!this.isActive)
return '';
const token: Token = this.accessToken;
if(token == undefined)
return '';
if(token == null)
return '';
if(!token.raw)
return '';
if(!token.is_jwt)
return '';
const access: object[] = token.payload['access'];
if( !!access &&
access.length > 0)
{
const scopeIdx = access.findIndex(e => e['app'] == appId)
if(scopeIdx < 0)
return '';
const accessScope: object = access[scopeIdx];
const scopeTenants = accessScope['tenants'];
if(!scopeTenants || scopeTenants.length == 0)
return '';
for(let i=0; i<scopeTenants.length; i++)
{
const tenantInfo = scopeTenants[i];
if(typeof tenantInfo === 'object' && tenantInfo !== null)
{
if(tenantInfo['tid'] == this.tid)
{
if(!tenantInfo.details)
return '';
const accessDetails = JSON.parse(tenantInfo.details);
return accessDetails.role ?? '';
}
}
}
return '';
}
else
return '';
}
public authAccessGroup() : number
{
return this.accessGroup("auth");
}
public filesAccessGroup() : number
{
return this.accessGroup("files");
}
private accessGroup(scope: string) : number
{
if(!this.isActive)
return 0;
const token: Token = this.accessToken;
if(token == undefined)
return 0;
if(token == null)
return 0;
if(!token.raw)
return 0;
if(!token.is_jwt)
return 0;
const access: object[] = token.payload['access'];
if( !!access &&
access.length > 0)
{
const scopeIdx = access.findIndex(e => e['app'] == scope)
if(scopeIdx < 0)
return 0;
const accessScope: object = access[scopeIdx];
const scopeTenants = accessScope['tenants'];
if(!scopeTenants || scopeTenants.length == 0)
return 0;
for(let i=0; i<scopeTenants.length; i++)
{
const tenantInfo = scopeTenants[i];
if(typeof tenantInfo === 'object' && tenantInfo !== null)
{
if(tenantInfo['tid'] == this.tid)
return tenantInfo['gid'] ?? 0;
}
}
return 0;
}
else
return 0;
}
public async __is_admin() :Promise<boolean>
{
if(!this.isValid)
this.validate();
if(!this.isActive)
return false;
if(this.tid == "")
return false;
let path :string;
path = this.configuration.iss + "/auth/am_i_admin";
path += "?tenant=" + this.tid;
const res = await fetch( path,
{
method: 'get',
headers : new Headers({
'Authorization':'Bearer ' + this._access_token.raw,
'Accept': 'application/json'})
});
if(!res.ok)
return false;
const result = await res.json();
return result.response === true;
}
public get mode() :Mode
{
let num_mode :number = 0;
if(!this.storage.get_num('_hd_auth_session_mode', (v) => { num_mode = v; }))
return Mode.Remote;
else switch(num_mode)
{
case 0:
return Mode.Remote;
case 1:
return Mode.Local;
case 2:
return Mode.Disabled;
default:
return Mode.Remote;
}
}
public set mode( m :Mode)
{
switch(m)
{
case Mode.Remote:
this.storage.set_num("_hd_auth_session_mode", 0);
break;
case Mode.Local:
this.storage.set_num("_hd_auth_session_mode", 1);
break;
case Mode.Disabled:
this.storage.set_num("_hd_auth_session_mode", 2);
break;
}
}
public get remote() :boolean
{
return (this.mode == Mode.Remote);
}
public get local() :boolean
{
return (this.mode == Mode.Local);
}
public get disabled() :boolean
{
return (this.mode == Mode.Disabled);
}
protected setup_mode(m :Mode)
{
let was_remote :boolean = this.mode == Mode.Remote;
//this.signout();
this.mode = m;
if(m==Mode.Remote)
{
/*let org_api_addr :string;
if(this.storage.get("_hd_auth_org_api_address", (v)=>{org_api_addr=v;}))
{
this.storage.set("_hd_auth_api_address", org_api_addr);
this.storage.set("_hd_auth_org_api_address", '');
}
*/
}
else
{
if(this.configuration && this.configuration.local_api)
{
//this.storage.set("_hd_auth_org_api_address", '');
//this.storage.set("_hd_auth_api_address", this.configuration.local_api);
//this.setCurrentTenantAPI(this.configuration.local_api, '')
}
}
}
public setLocalDevCurrentUser(email :string)
{
this.storage.set('_hd_auth_local_dev_user', email);
this.boost_validation_ticket();
this.validate();
let new_session :Session = new Session(this.storage);
new_session.clone_from(this);
session.set(new_session); // forces store subscribers
}
public get localDevCurrentUser() :Local_user
{
let email :string;
this.storage.get('_hd_auth_local_dev_user', (v)=>{email=v;});
if(!email)
return null;
const foundUser = this.configuration.local_users.find(u => u.username == email)
return foundUser;
}
}
export const session :Writable<Session> = writable(new Session(gv));