@datalayer/core
Version:
[](https://datalayer.io)
239 lines (238 loc) • 9.21 kB
JavaScript
/*
* Copyright (c) 2023-2025 Datalayer, Inc.
* Distributed under the terms of the Modified BSD License.
*/
/*
* Copyright (c) 2023-2025 Datalayer, Inc.
*
* Datalayer License
*/
/**
* Authentication strategy implementations
*/
import * as authentication from '../../api/iam/authentication';
import * as profile from '../../api/iam/profile';
import { UserDTO } from '../../models/UserDTO';
/**
* Base authentication strategy with common functionality
*/
class BaseAuthStrategy {
iamRunUrl;
storage;
constructor(iamRunUrl, storage) {
this.iamRunUrl = iamRunUrl;
this.storage = storage;
}
/**
* Validate a token by calling whoami
*/
async validateToken(token) {
const response = await profile.whoami(token, this.iamRunUrl);
if (!response || !response.profile) {
throw new Error('Invalid response from profile API');
}
const userData = {
id: response.profile.id,
uid: response.profile.uid,
handle_s: response.profile.handle_s || response.profile.handle,
email_s: response.profile.email_s || response.profile.email,
first_name_t: response.profile.first_name_t || response.profile.first_name || '',
last_name_t: response.profile.last_name_t || response.profile.last_name || '',
avatar_url_s: response.profile.avatar_url_s || response.profile.avatar_url,
};
return new UserDTO(userData, undefined);
}
}
/**
* Token-based authentication strategy
* Authenticates using an existing token
*/
export class TokenAuthStrategy extends BaseAuthStrategy {
canHandle(options) {
return !!options.token;
}
async authenticate(options) {
if (!options.token) {
throw new Error('Token is required for token authentication');
}
// Validate the token
const user = await this.validateToken(options.token);
// Store the token if requested
if (!options.noStore && this.storage) {
if (this.storage.setToken) {
await this.storage.setToken(options.token);
}
if (this.storage.setUser) {
await this.storage.setUser(user);
}
}
return { user, token: options.token };
}
}
/**
* Credentials-based authentication strategy
* Authenticates using handle and password
*/
export class CredentialsAuthStrategy extends BaseAuthStrategy {
canHandle(options) {
return !!options.handle && !!options.password;
}
async authenticate(options) {
if (!options.handle || !options.password) {
throw new Error('Handle and password are required for credentials authentication');
}
// Call the login API
const response = await authentication.login({ handle: options.handle, password: options.password }, this.iamRunUrl);
if (!response || !response.success || !response.token) {
throw new Error(response?.message || 'Login failed');
}
const token = response.token;
// Get user profile
const user = await this.validateToken(token);
// Store the token if requested
if (!options.noStore && this.storage) {
this.storage.setToken?.(token);
this.storage.setUser?.(user);
}
return { user, token };
}
}
/**
* Storage-based authentication strategy
* Authenticates using a token from storage
*/
export class StorageAuthStrategy extends BaseAuthStrategy {
canHandle(options) {
// Can handle if:
// 1. Storage is available
// 2. No other auth method is provided (this is the fallback strategy)
if (!this.storage || !this.storage.isAvailable()) {
return false;
}
// Return true if no explicit auth method provided (empty options or noStore only)
// This allows the authenticate() method to check storage async
const hasExplicitAuthMethod = !!options.token ||
!!options.handle ||
!!options.password ||
!!options.useBrowser;
return !hasExplicitAuthMethod;
}
async authenticate(options) {
if (!this.storage) {
throw new Error('Storage is required for storage-based authentication');
}
// Try async getTokenAsync first (for VS Code async keytar support)
let token = null;
if ('getTokenAsync' in this.storage &&
typeof this.storage.getTokenAsync === 'function') {
token = await this.storage.getTokenAsync();
}
else if (this.storage.getToken) {
token = this.storage.getToken();
}
if (!token) {
throw new Error('No token found in storage');
}
// Validate the token
const user = await this.validateToken(token);
return { user, token };
}
}
/**
* Browser OAuth strategy
* Authenticates using browser-based OAuth flow with GitHub or LinkedIn
*/
export class BrowserOAuthStrategy extends BaseAuthStrategy {
canHandle(options) {
return !!options.useBrowser;
}
async authenticate(options) {
// Import OAuth2 API
const { getOAuth2AuthzUrl } = await import('../../api/iam/oauth2');
// Default to GitHub provider
const provider = options.oauthProvider || 'github';
if (provider !== 'github' && provider !== 'linkedin') {
throw new Error(`Unsupported OAuth provider: ${provider}. Use 'github' or 'linkedin'.`);
}
// Generate callback URI
const callbackUri = options.callbackUri || `${window.location.origin}/auth/callback`;
try {
// Step 1: Get OAuth authorization URL
const authzResponse = await getOAuth2AuthzUrl(provider, callbackUri, this.iamRunUrl, options.nonce);
// Step 2: Open OAuth URL in popup or redirect
if (options.usePopup !== false) {
// Use popup window
const popup = window.open(authzResponse.loginURL, 'oauth-login', 'width=600,height=700,left=100,top=100');
if (!popup) {
throw new Error('Failed to open OAuth popup. Please allow popups for this site.');
}
// Wait for callback - Datalayer redirects to our callback with ?user=<json>&token=<token>
const result = await this.waitForOAuthCallback(popup, callbackUri);
// Extract token and user from callback result
const token = result.token;
const user = result.user;
if (!token) {
throw new Error('No token received from OAuth callback');
}
if (!user) {
throw new Error('No user data received from OAuth callback');
}
// Store token if requested
if (!options.noStore && this.storage) {
this.storage.setToken?.(token);
this.storage.setUser?.(user);
}
return { user, token };
}
else {
// Redirect to OAuth URL
window.location.href = authzResponse.loginURL;
// Return a pending promise (page will redirect)
return new Promise(() => {
// This promise never resolves because we redirect
});
}
}
catch (error) {
throw new Error(`OAuth authentication failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Wait for OAuth callback in popup window
* Expects: { user, token, error } from the Datalayer OAuth callback
*/
async waitForOAuthCallback(popup, callbackUri) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
popup.close();
reject(new Error('OAuth authentication timed out'));
}, 5 * 60 * 1000); // 5 minute timeout
// Listen for message from popup
const handleMessage = (event) => {
// Verify origin
if (event.origin !== new URL(callbackUri).origin) {
return;
}
clearTimeout(timeout);
window.removeEventListener('message', handleMessage);
popup.close();
if (event.data.error) {
reject(new Error(`OAuth error: ${event.data.error}`));
}
else {
resolve(event.data);
}
};
window.addEventListener('message', handleMessage);
// Check if popup was closed manually
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
clearTimeout(timeout);
window.removeEventListener('message', handleMessage);
reject(new Error('OAuth popup was closed'));
}
}, 1000);
});
}
}