openvibers/openvibe.shared auth-client.js
auth-client.js
| 1 | ; |
| 2 | |
| 3 | // ═══════════════════════════════════════════════════════════════ |
| 4 | // OpenVibe — OAuth2 Auth Client |
| 5 | // Used by OpenVibe.Live, OpenVibe.Tools, OpenVibe.Games, and OpenVibe.Media |
| 6 | // to authenticate users via the central OpenVibe.Network SSO. |
| 7 | // ═══════════════════════════════════════════════════════════════ |
| 8 | |
| 9 | const crypto = require('crypto'); |
| 10 | const jwt = require('jsonwebtoken'); |
| 11 | const { BRAND } = require('./brand'); |
| 12 | |
| 13 | const AUTH_BASE = BRAND.urls.login; |
| 14 | |
| 15 | class OpenVibeAuthClient { |
| 16 | /** |
| 17 | * @param {Object} opts |
| 18 | * @param {string} opts.clientId - OAuth2 client_id (e.g. 'live') |
| 19 | * @param {string} opts.clientSecret - OAuth2 client_secret |
| 20 | * @param {string} opts.redirectUri - Callback URL (e.g. 'https://openvibe.live/auth/callback') |
| 21 | * @param {string} opts.publicKey - RS256 public key for JWT verification (PEM) |
| 22 | * @param {string} [opts.authBase] - Override auth server URL (default: openvibe.network) |
| 23 | * @param {string} [opts.internalBase] - Internal auth API (default: http://127.0.0.1:4000) |
| 24 | */ |
| 25 | constructor(opts) { |
| 26 | this.clientId = opts.clientId; |
| 27 | this.clientSecret = opts.clientSecret; |
| 28 | this.redirectUri = opts.redirectUri; |
| 29 | this.publicKey = opts.publicKey; |
| 30 | this.authBase = opts.authBase || AUTH_BASE; |
| 31 | this.internalBase = opts.internalBase || 'http://127.0.0.1:4000'; |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * Generate an authorization URL for the OAuth2 login redirect. |
| 36 | * @param {string} [scope='profile theme'] - Requested scopes |
| 37 | * @returns {{ url: string, state: string }} |
| 38 | */ |
| 39 | getAuthorizationUrl(scope = 'profile theme') { |
| 40 | const state = crypto.randomBytes(16).toString('hex'); |
| 41 | const params = new URLSearchParams({ |
| 42 | client_id: this.clientId, |
| 43 | redirect_uri: this.redirectUri, |
| 44 | response_type: 'code', |
| 45 | scope, |
| 46 | state, |
| 47 | }); |
| 48 | return { |
| 49 | url: `${this.authBase}/oauth/authorize?${params.toString()}`, |
| 50 | state, |
| 51 | }; |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Exchange an authorization code for tokens. |
| 56 | * @param {string} code - The authorization code from the callback |
| 57 | * @returns {Promise<{ access_token: string, refresh_token: string, user: Object }>} |
| 58 | */ |
| 59 | async exchangeCode(code) { |
| 60 | const res = await fetch(`${this.authBase}/oauth/token`, { |
| 61 | method: 'POST', |
| 62 | headers: { 'Content-Type': 'application/json' }, |
| 63 | body: JSON.stringify({ |
| 64 | grant_type: 'authorization_code', |
| 65 | client_id: this.clientId, |
| 66 | client_secret: this.clientSecret, |
| 67 | redirect_uri: this.redirectUri, |
| 68 | code, |
| 69 | }), |
| 70 | }); |
| 71 | if (!res.ok) { |
| 72 | const err = await res.json().catch(() => ({})); |
| 73 | throw new Error(err.error || `Token exchange failed: ${res.status}`); |
| 74 | } |
| 75 | return res.json(); |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Refresh an access token using a refresh token. |
| 80 | * @param {string} refreshToken |
| 81 | * @returns {Promise<{ access_token: string, refresh_token: string }>} |
| 82 | */ |
| 83 | async refreshAccessToken(refreshToken) { |
| 84 | const res = await fetch(`${this.authBase}/oauth/token`, { |
| 85 | method: 'POST', |
| 86 | headers: { 'Content-Type': 'application/json' }, |
| 87 | body: JSON.stringify({ |
| 88 | grant_type: 'refresh_token', |
| 89 | client_id: this.clientId, |
| 90 | client_secret: this.clientSecret, |
| 91 | refresh_token: refreshToken, |
| 92 | }), |
| 93 | }); |
| 94 | if (!res.ok) { |
| 95 | const err = await res.json().catch(() => ({})); |
| 96 | throw new Error(err.error || `Token refresh failed: ${res.status}`); |
| 97 | } |
| 98 | return res.json(); |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Verify and decode a JWT access token. |
| 103 | * Uses the RS256 public key from the auth server. |
| 104 | * @param {string} token |
| 105 | * @returns {Object|null} Decoded payload or null if invalid |
| 106 | */ |
| 107 | verifyToken(token) { |
| 108 | try { |
| 109 | // issuer is the tools/SSO public URL — derive from authBase to support white-label |
| 110 | const issuer = this.authBase || this.issuer; |
| 111 | return jwt.verify(token, this.publicKey, { |
| 112 | algorithms: ['RS256'], |
| 113 | issuer, |
| 114 | }); |
| 115 | } catch { |
| 116 | return null; |
| 117 | } |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | module.exports = { OpenVibeAuthClient }; |
| 122 |