-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathdevice.ts
More file actions
153 lines (127 loc) · 5.16 KB
/
device.ts
File metadata and controls
153 lines (127 loc) · 5.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { Flags } from '@oclif/core';
import { DeviceOauthService, Messages, OAuth2Config } from '@salesforce/core';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as Transport from 'jsforce/lib/transport';
import { cli } from 'cli-ux';
import Command from '../../../lib/base';
import { herokuVariant } from '../../../lib/heroku-variant';
// This is a public Oauth client created expressly for the purpose of headless auth in the functions CLI.
// It does not require a client secret, is marked as public in the database and scoped accordingly
const PUBLIC_CLIENT_ID = '1e9cdca9-cec7-4dbf-ae84-408694b22bac';
Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-functions', 'login.functions.device');
export default class DeviceLogin extends Command {
static summary = messages.getMessage('summary');
static description = messages.getMessage('description');
static examples = messages.getMessages('examples');
static flags = {
'instance-url': Flags.string({
char: 'l',
description: messages.getMessage('flags.instance-url.summary'),
exclusive: ['instanceurl'],
}),
instanceurl: Flags.string({
char: 'l',
description: messages.getMessage('flags.instance-url.summary'),
exclusive: ['instance-url'],
hidden: true,
}),
alias: Flags.string({
char: 'a',
description: messages.getMessage('flags.alias.summary'),
}),
'set-default': Flags.boolean({
char: 'd',
description: messages.getMessage('flags.set-default.summary'),
}),
'set-default-dev-hub': Flags.boolean({
char: 'v',
description: messages.getMessage('flags.set-default-dev-hub.summary'),
}),
};
async run() {
const { flags } = await this.parse(DeviceLogin);
this.postParseHook(flags);
// We support both versions of the flag here for the sake of backward compat
const instanceUrl = flags['instance-url'] ?? flags.instanceurl;
if (flags.instanceurl) {
this.warn(messages.getMessage('flags.instanceurl.deprecation'));
}
cli.action.start('Logging in via device flow');
const oauthConfig: OAuth2Config = { loginUrl: instanceUrl };
const deviceOauthService: DeviceOauthService = await DeviceOauthService.create(oauthConfig);
const loginData = await deviceOauthService.requestDeviceLogin();
this.log(`Log in at: ${loginData.verification_uri}?user_code=${loginData.user_code}`);
const approval = await deviceOauthService.awaitDeviceApproval(loginData);
if (!approval) {
this.error('401 Unauthorized');
}
const authInfo = await deviceOauthService.authorizeAndSave(approval);
await authInfo.handleAliasAndDefaultSettings({
alias: flags.alias,
setDefault: flags['set-default'],
setDefaultDevHub: flags['set-default-dev-hub'],
});
await authInfo.save();
// Obtain sfdx access token from Auth info
const authFields = authInfo.getFields(true);
const token = authFields.accessToken;
// Fire off request to /oauth/tokens on the heroku side with JWT in the payload and
// obtain heroku access_token. This is configurable so that we can also target staging
const herokuClientId = process.env.SALESFORCE_FUNCTIONS_PUBLIC_OAUTH_CLIENT_ID ?? PUBLIC_CLIENT_ID;
let rawResponse;
try {
rawResponse = await new Transport().httpRequest({
method: 'POST',
url: `${process.env.SALESFORCE_FUNCTIONS_API || 'https://api.heroku.com'}/oauth/tokens`,
body: JSON.stringify({
client: {
id: herokuClientId,
},
grant: {
type: 'urn:ietf:params:oauth:grant-type:token-exchange',
},
subject_token: token,
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
}),
headers: { ...herokuVariant('salesforce_sso') },
});
} catch (e: any) {
const error = e as Error;
if (error.message?.includes('404')) {
this.error('No functions connection');
}
if (error.message?.includes('403')) {
this.error('User has not been provisioned yet, try $ sf login functions');
}
this.error(error);
}
const data = JSON.parse(rawResponse.body);
const bearerToken = data.access_token.token;
// We have to blow away the auth and API client objects so that they'll fully reinitialize with
// the new heroku credentials we're about to generate
this.resetClientAuth();
this.stateAggregator.tokens.set(Command.TOKEN_BEARER_KEY, {
token: bearerToken,
url: this.identityUrl.toString(),
user: authInfo.getUsername(),
});
await this.stateAggregator.tokens.write();
cli.action.stop();
return {
username: authFields.username,
sfdxAccessToken: token,
functionsAccessToken: bearerToken,
instanceUrl: authFields.instanceUrl,
orgId: authFields.orgId,
privateKey: authFields.privateKey,
};
}
}