Copy class AuthClient {
constructor(config) {
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.authorityUrl = config.authorityUrl;
this.session = new AuthSession();
}
async login() {
const codeVerifier = this.generateCodeVerifier();
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
const state = this.generateCodeVerifier();
sessionStorage.setItem('pkce_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `${this.authorityUrl}/authorize?${params}`;
}
async handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state');
}
const codeVerifier = sessionStorage.getItem('pkce_verifier');
const tokens = await this.exchangeCode(code, codeVerifier);
const user = await this.getUserInfo(tokens.access_token);
this.session.login(tokens, user);
// Clean up
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
window.history.replaceState({}, '', window.location.pathname);
return user;
}
async exchangeCode(code, codeVerifier) {
const response = await fetch(`${this.authorityUrl}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
code_verifier: codeVerifier
})
});
return response.json();
}
async getUserInfo(accessToken) {
const response = await fetch(`${this.authorityUrl}/userinfo`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
return response.json();
}
logout() {
this.session.logout();
}
isAuthenticated() {
return this.session.getAccessToken() !== null;
}
getUser() {
return this.session.getUser();
}
// PKCE helpers
generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return this.base64URLEncode(array);
}
async generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return this.base64URLEncode(new Uint8Array(hash));
}
base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
}
// Usage
const auth = new AuthClient({
clientId: 'your_client_id',
redirectUri: 'http://localhost:3000/callback',
authorityUrl: 'http://localhost:4000'
});
if (window.location.search.includes('code=')) {
auth.handleCallback().then(user => {
console.log('Logged in as:', user.name);
});
}