Skip to content

Commit

Permalink
fix: skip login page when authenticating with SSO
Browse files Browse the repository at this point in the history
When logging in with GitHub or GitLab, go straight to the appropriate
target site, instead of landing on getappmap.com/login.
  • Loading branch information
apotterri committed Oct 27, 2024
1 parent 339d7ac commit b350cae
Show file tree
Hide file tree
Showing 11 changed files with 50 additions and 27 deletions.
19 changes: 13 additions & 6 deletions src/authentication/appmapServerAuthenticationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ export default class AppMapServerAuthenticationProvider implements vscode.Authen
}
}

async createSession(): Promise<vscode.AuthenticationSession> {
async createSession(scopes: string[]): Promise<vscode.AuthenticationSession> {
const session = this.consumePendingLicenseKey();
if (session) {
this.session = session;
} else if (!this.session) {
this.session = await this.performSignIn();
this.session = await this.performSignIn(scopes);
debug('createSession(); session %savailable', this.session ? '' : 'not ');
}

Expand Down Expand Up @@ -159,20 +159,27 @@ export default class AppMapServerAuthenticationProvider implements vscode.Authen
return;
}

async performSignIn(): Promise<vscode.AuthenticationSession | undefined> {
async performSignIn(scopes: string[]): Promise<vscode.AuthenticationSession | undefined> {
const nonce = randomUUID();
const authnHandler = new AppMapServerAuthenticationHandler(nonce);
const authnStrategies = [
new VscodeProtocolRedirect(this.uriHandler, authnHandler),
new LocalWebserver(authnHandler),
];

const ssoTarget: string | undefined = scopes
.find((s) => s.startsWith('ssoTarget:'))
?.split(':')[1];

for (const [index, authnStrategy] of authnStrategies.entries()) {
authnStrategy.prepareSignIn();
const redirectUri = await authnStrategy.redirectUrl([['nonce', nonce]]);
const authnUrl = AppMapServerAuthenticationProvider.authURL(authnStrategy.authnPath, {
redirect_url: redirectUri.toString(),
});
const authnUrl = AppMapServerAuthenticationProvider.authURL(
authnStrategy.getAuthnPath(ssoTarget),
{
redirect_url: redirectUri.toString(),
}
);
vscode.env.openExternal(authnUrl);
const session = await vscode.window.withProgress<vscode.AuthenticationSession | AuthFailure>(
{
Expand Down
2 changes: 1 addition & 1 deletion src/authentication/authenticationStrategy/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Disposable } from 'vscode';

export default interface AuthenticationStrategy extends Disposable {
authnPath: string;
getAuthnPath(ssoTarget?: string): string;
redirectUrl(params: ReadonlyArray<[string, string]>): string | Promise<string>;
prepareSignIn(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import loginPage from '../../../web/static/html/authn_success.html';
export default class LocalWebserver implements AuthenticationStrategy {
private server?: Server;

public get authnPath(): string {
public getAuthnPath(): string {
return 'authn_provider/localhost';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as vscode from 'vscode';
import AuthenticationStrategy from '.';
import extensionSettings from '../../configuration/extensionSettings';
import AppMapServerAuthenticationHandler from '../../uri/appmapServerAuthenticationHandler';
import UriHandler from '../../uri/uriHandler';

export default class VscodeProtocolRedirect implements AuthenticationStrategy {
public get authnPath(): string {
return 'authn_provider/vscode';
public getAuthnPath(ssoTarget?: string): string {
let path = 'authn_provider/vscode';
if (ssoTarget) {
path += `?ssoTarget=${ssoTarget}`;
}
return path;
}

constructor(
Expand All @@ -15,12 +18,11 @@ export default class VscodeProtocolRedirect implements AuthenticationStrategy {
) {}

async redirectUrl(params: ReadonlyArray<[string, string]>): Promise<string> {
const query = params
.reduce((query, [k, v]) => {
query.append(k, v);
return query;
}, new URLSearchParams())
.toString();
const queryParams = params.reduce((query, [k, v]) => {
query.append(k, v);
return query;
}, new URLSearchParams());
const query = queryParams.toString();

const uri = vscode.Uri.parse(
`${vscode.env.uriScheme}://appland.appmap/authn-appmap-server`
Expand Down
11 changes: 9 additions & 2 deletions src/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ import Environment from '../configuration/environment';
// extension (because of Webpack), but fails during tests. As a quick fix, this export has been moved out.
export const AUTHN_PROVIDER_NAME = 'appmap.server';

export async function getApiKey(createIfNone: boolean): Promise<string | undefined> {
export async function getApiKey(
createIfNone: boolean,
ssoTarget?: string
): Promise<string | undefined> {
if (!createIfNone && Environment.appMapTestApiKey) return Environment.appMapTestApiKey;

let session: vscode.AuthenticationSession | undefined;
try {
session = await vscode.authentication.getSession(AUTHN_PROVIDER_NAME, ['default'], {
const scopes = ['default'];
if (ssoTarget) {
scopes.push(`ssoTarget:${ssoTarget}`);
}
session = await vscode.authentication.getSession(AUTHN_PROVIDER_NAME, scopes, {
createIfNone,
});
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<AppMap
);

vscode.commands.registerCommand('appmap.login', async () => {
appmapServerAuthenticationProvider.createSession();
appmapServerAuthenticationProvider.createSession([]);
});
vscode.commands.registerCommand('appmap.logout', async () => {
appmapServerAuthenticationProvider.removeSession();
Expand Down
4 changes: 2 additions & 2 deletions src/services/signInManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export default class SignInManager {
});
}

public static async signIn(): Promise<void> {
public static async signIn(ssoTarget?: string): Promise<void> {
if (this.signedIn) return;

try {
this.signedIn = !!(await getApiKey(true));
this.signedIn = !!(await getApiKey(true, ssoTarget));
this.updateSignInState();
} catch (e) {
Telemetry.sendEvent(DEBUG_EXCEPTION, {
Expand Down
3 changes: 2 additions & 1 deletion src/webviews/signInWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ export default class SignInViewProvider implements vscode.WebviewViewProvider {
}

case 'sign-in': {
const ssoTarget = message.data;
this.authProvider.customCancellationToken.cancel();
// AHT: If there is a sign-in attempt in progress it does not get cancelled before the next sign-in attempt
// unless I delay the next attempt using setTimeout. Perhaps there is a race condition in VS Code.
setTimeout(() => SignInManager.signIn(), 500);
setTimeout(() => SignInManager.signIn(ssoTarget), 500);
break;
}

Expand Down
2 changes: 1 addition & 1 deletion test/integration/authentication/appmapServerLogin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('Authenticate', () => {
});

const obtainedSession =
await appmapService.appmapServerAuthenticationProvider.createSession();
await appmapService.appmapServerAuthenticationProvider.createSession([]);

assert(obtainedSession, 'session');
assert.deepStrictEqual(obtainedSession, session);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ describe('VscodeProtocolRedirect', () => {
});

it('should have the correct authnPath', () => {
expect(vscodeProtocolRedirect.authnPath).to.equal('authn_provider/vscode');
expect(vscodeProtocolRedirect.getAuthnPath()).to.equal('authn_provider/vscode');
});

it('should include the SSO target', () => {
expect(vscodeProtocolRedirect.getAuthnPath('github')).to.equal(
'authn_provider/vscode?ssoTarget=github'
);
});

it('should redirect to the correct URL', async () => {
Expand Down
4 changes: 2 additions & 2 deletions web/src/signInView.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export default function mountSignInView() {
},
});

app.$on('sign-in', () => {
messages.rpc('sign-in');
app.$on('sign-in', (ssoTarget) => {
messages.rpc('sign-in', ssoTarget);
});

app.$on('activate', (apiKey) => {
Expand Down

0 comments on commit b350cae

Please sign in to comment.