From 3863dccd45c386bb0a4c4d0958cf477e615c8d0c Mon Sep 17 00:00:00 2001 From: stevenplc <106053900+stevenplc@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:41:03 +0900 Subject: [PATCH] feat: add azure token refresh (#40) --- package.json | 5 +- src/core/identity.service.ts | 103 ++++++++++++--------- src/core/lemon-storage.service.ts | 61 ++++++++---- src/helper/types/lemon-oauth-token.type.ts | 2 + 4 files changed, 103 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index cb3c303..b9eb2a8 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/core/identity.service.ts b/src/core/identity.service.ts index a46d6e3..84c46d8 100644 --- a/src/core/identity.service.ts +++ b/src/core/identity.service.ts @@ -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; @@ -53,15 +54,15 @@ export class IdentityService { async buildCredentialsByToken(token: LemonOAuthTokenResult): Promise { 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); } } @@ -90,12 +91,14 @@ export class IdentityService { params: any = {}, bodyReq?: any ): Promise { - 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); @@ -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 } @@ -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); } } @@ -135,32 +142,29 @@ export class IdentityService { async getCredentials(): Promise { 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) { @@ -171,7 +175,7 @@ export class IdentityService { } async isAuthenticated(): Promise { - const isAWS = await this.lemonStorage.getItem(`accessKeyId`); + const isAWS = !!(await this.lemonStorage.getItem(`accessKeyId`)); const hasCachedToken = await this.lemonStorage.hasCachedToken(); if (!hasCachedToken) { @@ -260,7 +264,8 @@ export class IdentityService { async refreshCachedToken(): Promise { 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, @@ -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; } @@ -380,7 +391,7 @@ export class IdentityService { } } - private async buildAWSCredentialsByToken(token: LemonOAuthTokenResult): Promise { + private async buildAWSCredentialsByToken(token: LemonOAuthTokenResult, cloud: Cloud): Promise { const { credential } = token; const { AccessKeyId, SecretKey } = credential; if (!AccessKeyId) { @@ -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); } } diff --git a/src/core/lemon-storage.service.ts b/src/core/lemon-storage.service.ts index 98cf3f3..6492aaf 100644 --- a/src/core/lemon-storage.service.ts +++ b/src/core/lemon-storage.service.ts @@ -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; @@ -17,6 +17,7 @@ export class LemonStorageService { 'secretKey', 'sessionToken', 'expiredTime', + 'hostKey', ]; private prefix: string; private storageService: Storage; @@ -50,16 +51,20 @@ export class LemonStorageService { } async hasCachedToken(): Promise { + 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; } @@ -76,22 +81,32 @@ export class LemonStorageService { return { AccessKeyId, SecretKey, SessionToken } as LemonCredentials; } - async getCachedLemonOAuthToken(): Promise { + async getCachedLemonOAuthToken(cloud: Cloud): Promise { 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; } @@ -101,21 +116,27 @@ export class LemonStorageService { * @param token * @returns */ - async saveLemonOAuthToken(token: LemonOAuthTokenResult): Promise { - const { accountId, authId, credential, identityId, identityPoolId, identityToken } = token; - const { AccessKeyId, SecretKey, SessionToken } = credential; + async saveLemonOAuthToken(token: LemonOAuthTokenResult, cloud: Cloud): Promise { + 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 diff --git a/src/helper/types/lemon-oauth-token.type.ts b/src/helper/types/lemon-oauth-token.type.ts index c45648a..99d1aeb 100644 --- a/src/helper/types/lemon-oauth-token.type.ts +++ b/src/helper/types/lemon-oauth-token.type.ts @@ -6,6 +6,7 @@ export interface LemonOAuthTokenResult { identityToken: string; identityPoolId?: string; error?: any; + accessToken?: string; } export interface LemonCredentials { @@ -13,6 +14,7 @@ export interface LemonCredentials { SecretKey: string; Expiration?: string; SessionToken?: string; + hostKey?: string; } export interface LemonRefreshTokenResult {