Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improves re-login experience. Closes #342, #374 #358

Merged
merged 3 commits into from
Dec 9, 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
4 changes: 2 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export async function activate(context: vscode.ExtensionContext) {

TerminalCommandExecuter.register();

AuthProvider.register();
AuthProvider.register(context);

Dependencies.registerCommands();
Scaffolder.registerCommands();
CliActions.registerCommands();
EntraAppRegistration.registerCommands();
EntraAppRegistration.registerCommands(context);

CommandPanel.register();

Expand Down
117 changes: 64 additions & 53 deletions src/providers/AuthProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EnvironmentInformation } from '../services/dataType/EnvironmentInformation';
import { env, authentication, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationSession, AuthenticationSessionAccountInformation, commands, Disposable, Event, EventEmitter, ProgressLocation, window, Progress } from 'vscode';
import * as vscode from 'vscode';
import { Commands } from '../constants';
import { Logger } from '../services/dataType/Logger';
import { Notifications } from '../services/dataType/Notifications';
Expand All @@ -20,20 +21,23 @@ export class M365AuthenticationSession implements AuthenticationSession {
public tenantId: string = '';
public clientId: string = '';

constructor(public readonly account: AuthenticationSessionAccountInformation) {}
constructor(public readonly account: AuthenticationSessionAccountInformation) { }
}

export class AuthProvider implements AuthenticationProvider, Disposable {
public static readonly id = 'm365-pnp-auth-dev';
public static instance: AuthProvider;
public static reSignIn = false;

private static context: vscode.ExtensionContext;
private onDidChangeEventEmit = new EventEmitter<AuthenticationProviderAuthenticationSessionsChangeEvent>();
private initializedDisposable: Disposable | undefined;

/**
* Registers the authentication provider and associated commands.
*/
public static register() {
public static register(context: vscode.ExtensionContext) {
AuthProvider.context = context;
const ext = Extension.getInstance();
const subscriptions = ext.subscriptions;

Expand Down Expand Up @@ -88,6 +92,7 @@ export class AuthProvider implements AuthenticationProvider, Disposable {
return;
}

EnvironmentInformation.clientId = await AuthProvider.context.globalState.get('clientId');
const clientId = await window.showInputBox({
title: 'Specify the application (client) ID',
value: EnvironmentInformation.clientId ?? '',
Expand All @@ -108,9 +113,11 @@ export class AuthProvider implements AuthenticationProvider, Disposable {

if (!clientId) {
Logger.error('Client ID is required');
EnvironmentInformation.clientId = undefined;
throw new Error('Client ID is required');
}

EnvironmentInformation.tenantId = await AuthProvider.context.globalState.get('tenantId');
const tenantId = await window.showInputBox({
title: 'Specify the tenant ID',
value: EnvironmentInformation.tenantId ?? '',
Expand All @@ -131,11 +138,15 @@ export class AuthProvider implements AuthenticationProvider, Disposable {

if (!tenantId) {
Logger.error('Tenant ID is required');
EnvironmentInformation.clientId = undefined;
EnvironmentInformation.tenantId = undefined;
throw new Error('Tenant ID is required');
}

EnvironmentInformation.clientId = clientId;
EnvironmentInformation.tenantId = tenantId;
await AuthProvider.context.globalState.update('clientId', clientId);
await AuthProvider.context.globalState.update('tenantId', tenantId);

await authentication.getSession(AuthProvider.id, [], { createIfNone });
}
Expand All @@ -145,6 +156,7 @@ export class AuthProvider implements AuthenticationProvider, Disposable {
* @param createIfNone - A boolean indicating whether to create a new session if none exists.
*/
public static async login(createIfNone: boolean = true) {
AuthProvider.reSignIn = false;
await authentication.getSession(AuthProvider.id, [], { createIfNone });
}

Expand Down Expand Up @@ -187,23 +199,9 @@ export class AuthProvider implements AuthenticationProvider, Disposable {
title: `Logging in to Microsoft 365. Check [output window](command:${Commands.showOutputChannel}) for more details`,
cancellable: true
}, async (progress: Progress<{ message?: string; increment?: number }>) => {
await executeCommand('login', { output: 'json', appId: clientId, tenant: tenantId }, {
await executeCommand('login', { output: 'json', appId: clientId, tenant: tenantId, authType: 'browser' }, {
stdout: (message: string) => {
if (message.includes('https://microsoft.com/devicelogin')) {
commands.executeCommand('vscode.open', 'https://microsoft.com/devicelogin');

const deviceCodeString = message.split('enter the code').pop();
if (deviceCodeString?.includes('to authenticate')) {
const deviceCode = deviceCodeString.split('to authenticate').shift()?.trim();
if (deviceCode) {
Logger.info(`Device code: ${deviceCode}`);
env.clipboard.writeText(deviceCode);
progress.report({ message: `Device code '${deviceCode}' copied to clipboard. [Open the browser to authenticate](https://microsoft.com/devicelogin)` });
}
} else {
Notifications.info(`Check [output window](command:${Commands.showOutputChannel}) for the device code.`);
}
}
Notifications.info('To sign in, use the web browser that just has been opened. Please sign-in there.');
return '';
},
stderr: (message: string) => {
Expand Down Expand Up @@ -261,43 +259,56 @@ export class AuthProvider implements AuthenticationProvider, Disposable {
*/
public async getAccount(): Promise<M365AuthenticationSession | undefined> {
if (!EnvironmentInformation.account) {
const status = await executeCommand('status', { output: 'json' });

if (status.stdout) {
Logger.info(`status: ${status.stdout}`);
const sessions = JSON.parse(status.stdout.toString());

if (sessions && sessions.connectedAs) {
EnvironmentInformation.account = sessions.connectedAs;

const account = new M365AuthenticationSession({
id: AuthProvider.id,
label: sessions.connectedAs
});

account.tenantId = sessions.appTenant ?? '';
EnvironmentInformation.tenantId = sessions.appTenant;
account.clientId = sessions.appId ?? '';
EnvironmentInformation.clientId = sessions.appId;

return account;
}
}

if (status.stderr) {
Logger.error(`status: ${status.stderr}`);
}
} else {
const account = new M365AuthenticationSession({
id: AuthProvider.id,
label: EnvironmentInformation.account
return await new Promise((resolve: (res: M365AuthenticationSession | undefined) => void, reject: (e: Error) => void): void => {
let account: M365AuthenticationSession | undefined;
executeCommand('status', { output: 'json' }, {
stdout: (message: string) => {
Logger.info(`status: ${message}`);
const sessions = JSON.parse(message.toString());

if (sessions && sessions.connectedAs) {
EnvironmentInformation.account = sessions.connectedAs;

account = new M365AuthenticationSession({
id: AuthProvider.id,
label: sessions.connectedAs
});
account.tenantId = sessions.appTenant ?? '';
EnvironmentInformation.tenantId = sessions.appTenant;
account.clientId = sessions.appId ?? '';
EnvironmentInformation.clientId = sessions.appId;
}
},
stderr: (message: string) => {
message = message.toString();
if (!AuthProvider.reSignIn && message.includes('Access token expired')) {
AuthProvider.logout();
AuthProvider.reSignIn = true;
const SignInButton = 'Sign in';
Notifications.info('Access token expired.', SignInButton).then((item) => {
if (item === SignInButton) {
AuthProvider.signIn();
}
});
} else {
Logger.error(`status: ${message}`);
}
}
}).then(() => {
resolve(account);
}).catch(error => {
reject(error);
});
});
account.tenantId = EnvironmentInformation.tenantId ?? '';
account.clientId = EnvironmentInformation.clientId ?? '';

return account;
}

return undefined;
const account = new M365AuthenticationSession({
id: AuthProvider.id,
label: EnvironmentInformation.account
});
account.tenantId = EnvironmentInformation.tenantId ?? '';
account.clientId = EnvironmentInformation.clientId ?? '';

return account;
}
}
8 changes: 7 additions & 1 deletion src/services/actions/EntraAppRegistration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { window, commands, ProgressLocation, Progress } from 'vscode';
import * as vscode from 'vscode';
import { Extension } from '../dataType/Extension';
import { Subscription } from '../../models';
import { Commands, WebViewType } from '../../constants';
Expand All @@ -11,8 +12,10 @@ import { AuthProvider } from '../../providers/AuthProvider';


export class EntraAppRegistration {
private static context: vscode.ExtensionContext;

public static registerCommands() {
public static registerCommands(context: vscode.ExtensionContext) {
EntraAppRegistration.context = context;
const subscriptions: Subscription[] = Extension.getInstance().subscriptions;

subscriptions.push(
Expand Down Expand Up @@ -60,6 +63,9 @@ export class EntraAppRegistration {
}
});

await EntraAppRegistration.context.globalState.update('clientId', EnvironmentInformation.clientId);
await EntraAppRegistration.context.globalState.update('tenantId', EnvironmentInformation.tenantId);

Notifications.info('SPFx Toolkit App Registration created successfully');
PnPWebview.close();
AuthProvider.signIn();
Expand Down