Skip to content
This repository has been archived by the owner on May 31, 2024. It is now read-only.

feat: add azure token refresh #40

Merged
merged 1 commit into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lemoncloud/lemon-front-lib",
"version": "1.4.1",
"version": "1.4.2",
"description": "Web Core Library for Lemoncloud",
"exports": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -19,7 +19,8 @@
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"fix:lint": "eslint src --ext .ts --fix",
"changeset": "npx changeset",
"changeset:version": "npx changeset version"
"changeset:version": "npx changeset version",
"build:pack": "pnpm build && pnpm pack"
},
"keywords": [
"lemoncloud",
Expand Down
103 changes: 57 additions & 46 deletions src/core/identity.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import * as AWS from 'aws-sdk/global.js';
// services
import { AxiosService, calcSignature, Cloud, createAsyncDelay, SignedHttpService } from '../helper';
import { LemonStorageService, Storage } from './lemon-storage.service';
import { AxiosService, calcSignature, createAsyncDelay, SignedHttpService } from '../helper';
// types
import { RequiredHttpParameters, SignaturePayload } from '../helper';
import { AxiosRequestConfig } from 'axios';
import {
LemonCredentials,
LemonOAuthTokenResult,
LemonOptions,
LemonRefreshTokenResult,
LoggerService,
RequiredHttpParameters,
SignaturePayload,
} from '../helper';
import { AxiosRequestConfig } from 'axios';

export class IdentityService {
private oauthURL: string;
Expand Down Expand Up @@ -53,15 +54,15 @@ export class IdentityService {
async buildCredentialsByToken(token: LemonOAuthTokenResult): Promise<void> {
this.logger.log('buildCredentialsByToken()...');
// TODO: refactor below. create class
const isAWS = this.options.cloud === 'aws';
const isAzure = this.options.cloud === 'azure';
if (isAWS) {
const cloud = this.options.cloud;

if (cloud === 'aws') {
this.logger.log('Using AWS platform');
return this.buildAWSCredentialsByToken(token);
return this.buildAWSCredentialsByToken(token, cloud);
}
if (isAzure) {
if (cloud === 'azure') {
this.logger.log('Using Azure platform');
return await this.lemonStorage.saveLemonOAuthToken(token);
return await this.lemonStorage.saveLemonOAuthToken(token, cloud);
}
}

Expand Down Expand Up @@ -90,12 +91,14 @@ export class IdentityService {
params: any = {},
bodyReq?: any
): Promise<any> {
const queryParams = { ...params };
const objParams: RequiredHttpParameters = { method, path, queryParams, bodyReq };

const isAWS = this.options.cloud === 'aws';
const isAzure = this.options.cloud === 'azure';
const identityToken = (await this.lemonStorage.getItem('identityToken')) || '';
let queryParams = { ...params };
let objParams: RequiredHttpParameters = { method, path, bodyReq };

if (isAWS) {
objParams = { method, path, queryParams, bodyReq };
if (!this.shouldUseXLemonIdentity) {
const options = { customHeader: this.extraHeader, customOptions: this.extraOptions };
const httpService = new SignedHttpService(options);
Expand All @@ -104,7 +107,6 @@ export class IdentityService {

// add X-Lemon-Identity
const identityId = (await this.lemonStorage.getItem('identityId')) || '';
const identityToken = (await this.lemonStorage.getItem('identityToken')) || '';
const shouldSetXLemonIdentity = !!identityId && !!identityToken;
const customHeader = shouldSetXLemonIdentity
? { ...this.extraHeader, 'x-lemon-identity': identityToken }
Expand All @@ -115,10 +117,15 @@ export class IdentityService {
}

if (isAzure) {
const accessToken = (await this.lemonStorage.getItem('identityToken')) || '';
const hostKey = (await this.lemonStorage.getItem('hostKey')) || '';
queryParams = { ...queryParams, code: hostKey, clientId: 'default' };
objParams = { ...objParams, queryParams: queryParams };
const accessToken = (await this.lemonStorage.getItem('accessToken')) || '';
const header = {
Authorization: `Bearer ${accessToken}`,
Authorization: `Bearer ${identityToken}`,
'x-lemon-identity': accessToken,
};

return this.executeRequest(header, endpoint, objParams);
}
}
Expand All @@ -135,32 +142,29 @@ export class IdentityService {

async getCredentials(): Promise<AWS.Credentials | null> {
const isAWS = this.options.cloud === 'aws';
const isAzure = this.options.cloud === 'azure';
if (isAzure) {
return null;

const hasCachedToken = await this.lemonStorage.hasCachedToken();
if (!hasCachedToken) {
this.logger.info('has no cached token!');
return new Promise(resolve => resolve(null));
}
if (isAWS) {
const hasCachedToken = await this.lemonStorage.hasCachedToken();
if (!hasCachedToken) {
this.logger.info('has no cached token!');
return new Promise(resolve => resolve(null));
}

const shouldRefreshToken = await this.lemonStorage.shouldRefreshToken();
if (shouldRefreshToken) {
this.logger.info('should refresh token!');
const refreshed = await this.refreshCachedToken();
if (refreshed) {
return await this.getCurrentCredentials();
}
const shouldRefreshToken = await this.lemonStorage.shouldRefreshToken();
if (shouldRefreshToken) {
this.logger.info('should refresh token!');
const refreshed = await this.refreshCachedToken();
if (refreshed) {
return await this.getCurrentCredentials();
}
}

const cachedToken = await this.lemonStorage.hasCachedToken();
if (!cachedToken) {
this.logger.info('has no cached token!');
return new Promise(resolve => resolve(null));
}
const cachedToken = await this.lemonStorage.hasCachedToken();
if (!cachedToken) {
this.logger.info('has no cached token!');
return new Promise(resolve => resolve(null));
}

if (isAWS) {
const credentials = AWS.config.credentials as AWS.Credentials;
const shouldRefresh = credentials.needsRefresh();
if (shouldRefresh) {
Expand All @@ -171,7 +175,7 @@ export class IdentityService {
}

async isAuthenticated(): Promise<boolean> {
const isAWS = await this.lemonStorage.getItem(`accessKeyId`);
const isAWS = !!(await this.lemonStorage.getItem(`accessKeyId`));

const hasCachedToken = await this.lemonStorage.hasCachedToken();
if (!hasCachedToken) {
Expand Down Expand Up @@ -260,7 +264,8 @@ export class IdentityService {

async refreshCachedToken(): Promise<LemonRefreshTokenResult | null> {
this.logger.log('refreshCachedToken()...');
const originToken: LemonOAuthTokenResult = await this.lemonStorage.getCachedLemonOAuthToken();
const cloud = this.options.cloud;
const originToken: LemonOAuthTokenResult = await this.lemonStorage.getCachedLemonOAuthToken(cloud);
const payload: SignaturePayload = {
authId: originToken.authId,
accountId: originToken.accountId,
Expand Down Expand Up @@ -294,16 +299,22 @@ export class IdentityService {
}

const { credential } = refreshResult;
const refreshToken: LemonOAuthTokenResult = {
let refreshToken: LemonOAuthTokenResult = {
...refreshResult,
identityPoolId: originToken.identityPoolId || '',
identityToken: originToken.identityToken,
};
await this.lemonStorage.saveLemonOAuthToken(refreshToken);
await this.lemonStorage.saveLemonOAuthToken(refreshToken, cloud);

if (cloud === 'aws') {
this.logger.log('create new credentials after refresh token');
refreshToken = {
...refreshResult,
identityPoolId: originToken.identityPoolId || '',
identityToken: originToken.identityToken,
};
IdentityService.createAWSCredentials(credential);
}

// AWS
this.logger.log('create new credentials after refresh token');
IdentityService.createAWSCredentials(credential);
return refreshResult;
}

Expand Down Expand Up @@ -380,7 +391,7 @@ export class IdentityService {
}
}

private async buildAWSCredentialsByToken(token: LemonOAuthTokenResult): Promise<void> {
private async buildAWSCredentialsByToken(token: LemonOAuthTokenResult, cloud: Cloud): Promise<void> {
const { credential } = token;
const { AccessKeyId, SecretKey } = credential;
if (!AccessKeyId) {
Expand All @@ -390,7 +401,7 @@ export class IdentityService {
throw new Error('.SecretKey (string) is required!');
}
this.logger.log('Using AWS platform');
await this.lemonStorage.saveLemonOAuthToken(token);
await this.lemonStorage.saveLemonOAuthToken(token, cloud);
return IdentityService.createAWSCredentials(credential);
}
}
61 changes: 41 additions & 20 deletions src/core/lemon-storage.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LemonCredentials, LemonOAuthTokenResult, LocalStorageService } from '../helper';
import { Cloud, LemonCredentials, LemonOAuthTokenResult, LocalStorageService } from '../helper';

export interface Storage {
getItem(key: string, ...params: any): any;
Expand All @@ -17,6 +17,7 @@ export class LemonStorageService {
'secretKey',
'sessionToken',
'expiredTime',
'hostKey',
];
private prefix: string;
private storageService: Storage;
Expand Down Expand Up @@ -50,16 +51,20 @@ export class LemonStorageService {
}

async hasCachedToken(): Promise<boolean> {
const expiredTime = await this.storageService.getItem(`${this.prefix}.expiredTime`);

// AWS
const accessKeyId = await this.storageService.getItem(`${this.prefix}.accessKeyId`);
const secretKey = await this.storageService.getItem(`${this.prefix}.secretKey`);
const expiredTime = await this.storageService.getItem(`${this.prefix}.expiredTime`);

// Azure
const identityToken = await this.storageService.getItem(`${this.prefix}.identityToken`);
const accessToken = await this.storageService.getItem(`${this.prefix}.accessToken`);
const hostKey = await this.storageService.getItem(`${this.prefix}.hostKey`);

const hasAwsToken = accessKeyId !== null && secretKey !== null && expiredTime !== null;
const hasAzureToken = identityToken !== null && expiredTime !== null;
const hasAzureToken =
identityToken !== null && accessToken !== null && hostKey !== null && expiredTime !== null;
return hasAwsToken || hasAzureToken;
}

Expand All @@ -76,22 +81,32 @@ export class LemonStorageService {
return { AccessKeyId, SecretKey, SessionToken } as LemonCredentials;
}

async getCachedLemonOAuthToken(): Promise<LemonOAuthTokenResult> {
async getCachedLemonOAuthToken(cloud: Cloud): Promise<LemonOAuthTokenResult> {
const result: any = await this.credentialItemList.reduce(async (promise, item) => {
const tmpResult: { [key: string]: string } = await promise.then();
tmpResult[item] = await this.storageService.getItem(`${this.prefix}.${item}`);
return Promise.resolve(tmpResult);
}, Promise.resolve({}));

const AccessKeyId = await this.storageService.getItem(`${this.prefix}.accessKeyId`);
const SecretKey = await this.storageService.getItem(`${this.prefix}.secretKey`);
const SessionToken = await this.storageService.getItem(`${this.prefix}.sessionToken`);
result.credential = { AccessKeyId, SecretKey, SessionToken };
if (cloud === 'aws') {
const AccessKeyId = await this.storageService.getItem(`${this.prefix}.accessKeyId`);
const SecretKey = await this.storageService.getItem(`${this.prefix}.secretKey`);
const SessionToken = await this.storageService.getItem(`${this.prefix}.sessionToken`);
result.credential = { AccessKeyId, SecretKey, SessionToken };

delete result.accessKeyId;
delete result.secretKey;
delete result.sessionToken;
delete result.expiredTime;
}

if (cloud === 'azure') {
const HostKey = await this.storageService.getItem(`${this.prefix}.hostKey`);
result.credential = { HostKey };

delete result.accessKeyId;
delete result.secretKey;
delete result.sessionToken;
delete result.expiredTime;
delete result.hostKey;
delete result.expiredTime;
}

return result;
}
Expand All @@ -101,21 +116,27 @@ export class LemonStorageService {
* @param token
* @returns
*/
async saveLemonOAuthToken(token: LemonOAuthTokenResult): Promise<void> {
const { accountId, authId, credential, identityId, identityPoolId, identityToken } = token;
const { AccessKeyId, SecretKey, SessionToken } = credential;
async saveLemonOAuthToken(token: LemonOAuthTokenResult, cloud: Cloud): Promise<void> {
const { accountId, authId, credential, identityId, identityPoolId, identityToken, accessToken } = token;
const { AccessKeyId, SecretKey, SessionToken, hostKey } = credential;

// save items...
this.storageService.setItem(`${this.prefix}.accountId`, accountId || '');
this.storageService.setItem(`${this.prefix}.authId`, authId || '');
this.storageService.setItem(`${this.prefix}.identityId`, identityId || '');
this.storageService.setItem(`${this.prefix}.identityPoolId`, identityPoolId || '');
this.storageService.setItem(`${this.prefix}.identityToken`, identityToken || '');

// credential for AWS
this.storageService.setItem(`${this.prefix}.accessKeyId`, AccessKeyId || '');
this.storageService.setItem(`${this.prefix}.secretKey`, SecretKey || '');
this.storageService.setItem(`${this.prefix}.sessionToken`, SessionToken || '');
if (cloud === 'aws') {
this.storageService.setItem(`${this.prefix}.identityPoolId`, identityPoolId || '');
this.storageService.setItem(`${this.prefix}.accessKeyId`, AccessKeyId || '');
this.storageService.setItem(`${this.prefix}.secretKey`, SecretKey || '');
this.storageService.setItem(`${this.prefix}.sessionToken`, SessionToken || '');
}

if (cloud === 'azure') {
this.storageService.setItem(`${this.prefix}.hostKey`, hostKey || '');
this.storageService.setItem(`${this.prefix}.accessToken`, accessToken || '');
}

// set expired time
const TIME_DELAY = 0.5; // 0.5 = 30minutes, 1 = 1hour
Expand Down
2 changes: 2 additions & 0 deletions src/helper/types/lemon-oauth-token.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ export interface LemonOAuthTokenResult {
identityToken: string;
identityPoolId?: string;
error?: any;
accessToken?: string;
}

export interface LemonCredentials {
AccessKeyId: string;
SecretKey: string;
Expiration?: string;
SessionToken?: string;
hostKey?: string;
}

export interface LemonRefreshTokenResult {
Expand Down