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

feat: Cache interactive login credentials #222

Merged
merged 13 commits into from
Aug 8, 2019
41 changes: 11 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/plugins/login/azureLoginPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import Serverless from "serverless";
import { AzureLoginOptions, AzureLoginService } from "../../services/loginService";
import { AzureBasePlugin } from "../azureBasePlugin";
import { loginHooks } from "./loginHooks";
import { SimpleFileTokenCache } from "./utils/simpleFileTokenCache";

export class AzureLoginPlugin extends AzureBasePlugin<AzureLoginOptions> {
private fileTokenCache: SimpleFileTokenCache;
public hooks: { [eventName: string]: Promise<any> };

public constructor(serverless: Serverless, options: AzureLoginOptions) {
super(serverless, options);

this.fileTokenCache = new SimpleFileTokenCache();

this.hooks = {};
for (const h of loginHooks) {
this.hooks[`before:${h}`] = this.login.bind(this);
Expand All @@ -24,12 +28,13 @@ export class AzureLoginPlugin extends AzureBasePlugin<AzureLoginOptions> {
this.log("Logging into Azure");

try {
wbreza marked this conversation as resolved.
Show resolved Hide resolved
const authResult = await AzureLoginService.login();
const authResult = await AzureLoginService.login({tokenCache: this.fileTokenCache});
this.serverless.variables["azureCredentials"] = authResult.credentials;
// Use environment variable for sub ID or use the first subscription in the list (service principal can
// have access to more than one subscription)
this.serverless.variables["subscriptionId"] = this.options.subscriptionId || process.env.azureSubId || this.serverless.service.provider["subscriptionId"] || authResult.subscriptions[0].id;
this.serverless.cli.log(`Using subscription ID: ${this.serverless.variables["subscriptionId"]}`);
this.fileTokenCache.addSubs(authResult.subscriptions);
}
catch (e) {
this.log("Error logging into azure");
Expand Down
92 changes: 92 additions & 0 deletions src/plugins/login/utils/simpleFileTokenCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as fs from "fs";
import path from "path";
import os from "os";

const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure");
const SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json");

export class SimpleFileTokenCache {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an actual interface type definition that we can reference here?

wbreza marked this conversation as resolved.
Show resolved Hide resolved
private tokens: any[] = [];
private subscriptions: any[] = [];
public constructor() {
console.log("new Simple file toke cache");
this.load();
}

public isSecureCache() {
throw "isSecureCache not implemented";
}

public add(entries: any, cb: any) {
console.log("adding")
this.tokens.push(...entries);
this.save();
cb();
}

public addSubs(entries: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does entries change? can we be more specific than just default to any?

console.log("adding")
this.subscriptions.push(...entries);
this.save();
}

public remove(entries: any, cb: any) {
console.log("remove")
this.tokens = this.tokens.filter(e => {
return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]);
});
this.save();
cb();
}

public clear(cb: any) {
console.log("clear");
wbreza marked this conversation as resolved.
Show resolved Hide resolved
this.tokens = [];
this.subscriptions = [];
this.save();
cb();
}

public find(query, cb) {
console.log("find")
let result = this.tokens.filter(e => {
return Object.keys(query).every(key => e[key] === query[key]);
});
cb(null, result);
return result;
}

public empty() {
console.log("empty")
// this.deleteOld();
wbreza marked this conversation as resolved.
Show resolved Hide resolved
return this.tokens.length === 0;
}

public first() {
return this.tokens[0];
}

public listSubscriptions() {
return this.subscriptions;
}

private load() {
try {
let savedCache = JSON.parse(fs.readFileSync(SLS_TOKEN_FILE).toString());
this.tokens = savedCache.tokens;
this.tokens.map(t => t.expiresOn = new Date(t.expiresOn))
this.subscriptions = savedCache.subscriptions;
} catch (e) {}
wbreza marked this conversation as resolved.
Show resolved Hide resolved
}

public save() {
console.log("save")
fs.writeFileSync(SLS_TOKEN_FILE, JSON.stringify({tokens: this.tokens, subscriptions: this.subscriptions}));
}

private deleteOld() {
wbreza marked this conversation as resolved.
Show resolved Hide resolved
this.tokens = this.tokens.filter(
t => t.expiresOn > Date.now() - 5 * 60 * 1000
);
}
}
3 changes: 2 additions & 1 deletion src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "../models/serverless";
import { Guard } from "../shared/guard";
import { Utils } from "../shared/utils";
import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache";

export abstract class BaseService {
protected baseUrl: string;
Expand Down Expand Up @@ -124,7 +125,7 @@ export abstract class BaseService {
* Get the access token from credentials token cache
*/
protected getAccessToken(): string {
return (this.credentials.tokenCache as any)._entries[0].accessToken;
return (this.credentials.tokenCache as SimpleFileTokenCache).first().accessToken;
wbreza marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
26 changes: 22 additions & 4 deletions src/services/loginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
loginWithServicePrincipalSecretWithAuthResponse,
AuthResponse,
AzureTokenCredentialsOptions,
InteractiveLoginOptions,
InteractiveLoginOptions,
DeviceTokenCredentials,
} from "@azure/ms-rest-nodeauth";
import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache";

export interface AzureLoginOptions extends Serverless.Options {
subscriptionId?: string;
Expand All @@ -32,9 +34,25 @@ export class AzureLoginService {
}
}

public static async interactiveLogin(options: InteractiveLoginOptions): Promise<AuthResponse> {
await open("https://microsoft.com/devicelogin");
return await interactiveLoginWithAuthResponse(options);
public static async interactiveLogin(options?: InteractiveLoginOptions): Promise<AuthResponse> {
wbreza marked this conversation as resolved.
Show resolved Hide resolved
// await open("https://microsoft.com/devicelogin");
// return await interactiveLoginWithAuthResponse(options);
var authResp: AuthResponse = {credentials: undefined, subscriptions: []};
if(!(options.tokenCache as SimpleFileTokenCache).empty()){
wbreza marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we try to use the normalized TokenCache interface vs. dropping down to the derived type?

console.log("exisitng token");
var devOptions = {
wbreza marked this conversation as resolved.
Show resolved Hide resolved
tokenCache: options.tokenCache as SimpleFileTokenCache
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case to SimpleFileTokenCache shouldn't be needed here.

}
// I don't think DeviceTokenCredentials is what we want... maybe MSITokenCredentials?
wbreza marked this conversation as resolved.
Show resolved Hide resolved
authResp.credentials = new DeviceTokenCredentials(undefined, undefined, devOptions.tokenCache.first().userId, undefined, undefined, options.tokenCache);
authResp.subscriptions = devOptions.tokenCache.listSubscriptions();
} else {
console.log("need to do interactive login now");
await open("https://microsoft.com/devicelogin");
authResp = await interactiveLoginWithAuthResponse(options);
}
return authResp;//iOptions ? interactiveLoginWithAuthResponse(iOptions) : interactiveLoginWithAuthResponse();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented code.


}

public static async servicePrincipalLogin(clientId: string, secret: string, tenantId: string, options: AzureTokenCredentialsOptions): Promise<AuthResponse> {
Expand Down