chore(dev): add stub OAuth IdP + seed + bridge smoketest for native SSO
Dev environments have no real OAuth provider configured, so GET /auth/providers returns [] and the native mobile SSO flow cannot be exercised locally. Add dependency-free dev tooling under scripts/dev/: - stub-idp.js: stub OAuth2/OIDC IdP (authorize picker, token, userinfo) - seed-sso-provider.js: registers a 'devstub' auth_providers row + pre-links each principal's sub to a dev account (SSO is link-only) - sso-bridge-smoketest.js: drives the full app flow headless (PKCE → start → IdP → callback → deep link → exchange) and asserts a bearer pair - README.md: host + emulator usage Verified end-to-end against the local site: player and admin principals both sign in and receive the correct role. DEV ONLY — never deploy the stub. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
105
scripts/dev/seed-sso-provider.js
Normal file
105
scripts/dev/seed-sso-provider.js
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Seed a dev SSO provider + pre-linked identities — DEV ONLY.
|
||||
*
|
||||
* Registers an `auth_providers` row pointing at the local `stub-idp.js`, so
|
||||
* `GET /auth/providers` returns a provider and the native mobile SSO flow becomes
|
||||
* exercisable. Because SSO is link-only (identities are never auto-provisioned),
|
||||
* it also pre-links each stub principal's `sub` to an existing dev account.
|
||||
*
|
||||
* Idempotent: re-running upserts the provider and skips already-linked identities.
|
||||
*
|
||||
* Run (loads website/server/.env for DB creds):
|
||||
* node website/scripts/dev/seed-sso-provider.js
|
||||
*
|
||||
* Env overrides:
|
||||
* STUB_PROVIDER_ID provider slug (default 'devstub')
|
||||
* STUB_IDP_PUBLIC_URL browser-facing base (default http://127.0.0.1:9099)
|
||||
* STUB_IDP_INTERNAL_URL server-facing base (default = STUB_IDP_PUBLIC_URL)
|
||||
*
|
||||
* The authorize URL is followed by the browser (Custom Tab); token/userinfo are
|
||||
* called server-side by the website. On an emulator set PUBLIC to the host's
|
||||
* reachable address (e.g. http://10.0.2.2:9099) and INTERNAL to http://127.0.0.1:9099.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// Load website/server/.env into process.env WITHOUT the dotenv dependency (it
|
||||
// lives in server/node_modules and wouldn't resolve from this scripts/ location).
|
||||
// The file is simple KEY=value; that is all we need for the DB credentials.
|
||||
function loadEnv(envPath) {
|
||||
if (!fs.existsSync(envPath)) return
|
||||
for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
||||
const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line)
|
||||
if (!m) continue
|
||||
const key = m[1]
|
||||
let val = m[2].trim()
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1)
|
||||
}
|
||||
if (process.env[key] === undefined) process.env[key] = val
|
||||
}
|
||||
}
|
||||
loadEnv(path.join(__dirname, '..', '..', 'server', '.env'))
|
||||
|
||||
const authProviders = require('../../server/src/model/authProviders/authProviders.model')
|
||||
const userIdentities = require('../../server/src/model/userIdentities/userIdentities.model')
|
||||
const { close } = require('../../server/src/utils/db')
|
||||
|
||||
const PROVIDER_ID = process.env.STUB_PROVIDER_ID || 'devstub'
|
||||
const PUBLIC_URL = (process.env.STUB_IDP_PUBLIC_URL || 'http://127.0.0.1:9099').replace(/\/$/, '')
|
||||
const INTERNAL_URL = (process.env.STUB_IDP_INTERNAL_URL || PUBLIC_URL).replace(/\/$/, '')
|
||||
|
||||
// sub → dev account id. Keep the subs in sync with stub-idp.js PRINCIPALS.
|
||||
const PRINCIPALS = [
|
||||
{ sub: 'stub-admin', email: 'admin@dev.local', userId: 1 },
|
||||
{ sub: 'stub-colby', email: 'colby@dev.local', userId: 14 },
|
||||
]
|
||||
|
||||
async function main() {
|
||||
// eslint-disable-next-line no-console
|
||||
const log = (...a) => console.log('[seed-sso]', ...a)
|
||||
|
||||
await authProviders.save(PROVIDER_ID, {
|
||||
kind: 'oauth2',
|
||||
name: 'Dev Stub IdP',
|
||||
enabled: true,
|
||||
clientId: 'stub-client',
|
||||
secret: 'stub-secret',
|
||||
authorizeUrl: `${PUBLIC_URL}/authorize`,
|
||||
tokenUrl: `${INTERNAL_URL}/token`,
|
||||
userinfoUrl: `${INTERNAL_URL}/userinfo`,
|
||||
scopes: 'openid email profile',
|
||||
priority: 50,
|
||||
})
|
||||
log(`provider '${PROVIDER_ID}' upserted (authorize=${PUBLIC_URL}/authorize, token/userinfo=${INTERNAL_URL})`)
|
||||
|
||||
for (const p of PRINCIPALS) {
|
||||
const existing = await userIdentities.findByProviderSubject(PROVIDER_ID, p.sub)
|
||||
if (existing) {
|
||||
log(`identity ${PROVIDER_ID}:${p.sub} already linked to user ${existing.user_id} — skip`)
|
||||
continue
|
||||
}
|
||||
await userIdentities.link({ userId: p.userId, provider: PROVIDER_ID, subject: p.sub, email: p.email })
|
||||
log(`linked ${PROVIDER_ID}:${p.sub} → user ${p.userId}`)
|
||||
}
|
||||
|
||||
log('done.')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[seed-sso] FAILED', err)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
try {
|
||||
await close()
|
||||
} catch {
|
||||
/* ignore shutdown errors */
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user