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

Commit

Permalink
feat: add azure token refresh (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenplc authored Jan 15, 2024
1 parent 12b778c commit 3863dcc
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 68 deletions.
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

0 comments on commit 3863dcc

Please sign in to comment.