Skip to content

Commit 7501efd

Browse files
authored
Merge pull request #760 from SolidOS/uvdsl
replace Inrupt by Uvdsl OIDC client
2 parents c242c32 + bd2212d commit 7501efd

9 files changed

Lines changed: 343 additions & 50 deletions

File tree

jest.config.mjs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
import { existsSync } from 'node:fs'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
5+
const __filename = fileURLToPath(import.meta.url)
6+
const __dirname = path.dirname(__filename)
7+
const localSolidLogicSrc = path.resolve(__dirname, '../solid-logic/src')
8+
const solidLogicMapper = existsSync(localSolidLogicSrc)
9+
? localSolidLogicSrc
10+
: '<rootDir>/node_modules/solid-logic/src'
11+
112
export default {
213
// verbose: true, // Uncomment for detailed test output
314
collectCoverage: true,
@@ -16,7 +27,9 @@ export default {
1627
setupFilesAfterEnv: ['./test/helpers/setup.ts'],
1728
moduleNameMapper: {
1829
'^~icons/(.*)$': '<rootDir>/__mocks__/iconsMock.js',
19-
'^.+\\.css$': '<rootDir>/__mocks__/styleMock.js'
30+
'^.+\\.css$': '<rootDir>/__mocks__/styleMock.js',
31+
'^solid-logic$': solidLogicMapper,
32+
'^@uvdsl/solid-oidc-client-browser$': '<rootDir>/test/mocks/solid-oidc-client-browser.ts'
2033
},
2134
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
2235
roots: ['<rootDir>/src', '<rootDir>/test', '<rootDir>/__mocks__'],

src/design-system/lib/auth/SolidAuth.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
import { authSession, solidLogicSingleton } from 'solid-logic'
22
import Account from '../../../primitives/lib/auth/Account'
3-
import { findImage } from '../../../widgets/buttons'
43
import { AuthContext } from '../../../primitives/lib/auth/context'
54
import { showDialog } from '../dialogs'
65
import { html } from 'lit'
6+
import ns from '../../../ns'
77

88
import '../../components/login-modal'
99

1010
export const DEFAULT_SIGNUP_URL = 'https://solidproject.org/get_a_pod'
1111

12+
function findAccountImage (webId: string): string | undefined {
13+
const store = solidLogicSingleton.store
14+
const me = store.sym(webId)
15+
const image =
16+
store.any(me, ns.sioc('avatar')) ||
17+
store.any(me, ns.foaf('img')) ||
18+
store.any(me, ns.vcard('logo')) ||
19+
store.any(me, ns.vcard('hasPhoto')) ||
20+
store.any(me, ns.vcard('photo')) ||
21+
store.any(me, ns.foaf('depiction'))
22+
23+
return image ? (image as any).value : undefined
24+
}
25+
1226
export default class SolidAuth implements AuthContext {
1327
constructor (public signupUrl: string = DEFAULT_SIGNUP_URL) {}
1428

1529
get account (): Account | null {
16-
if (!authSession.info?.isLoggedIn || !authSession.info?.webId) {
30+
const webId: string | undefined = authSession.webId ?? authSession.info?.webId
31+
const isActive: boolean = authSession.isActive ?? authSession.info?.isLoggedIn ?? Boolean(webId)
32+
if (!isActive || !webId) {
1733
return null
1834
}
1935

20-
const webId = authSession.info.webId
21-
const me = solidLogicSingleton.store.sym(webId)
22-
const avatarUrl = findImage(me) ?? undefined
36+
const avatarUrl = findAccountImage(webId)
2337

2438
return new Account(webId, avatarUrl)
2539
}
@@ -44,10 +58,7 @@ export default class SolidAuth implements AuthContext {
4458

4559
locationUrl.hash = ''
4660

47-
await authSession.login({
48-
redirectUrl: locationUrl.href,
49-
oidcIssuer: loginUrl
50-
})
61+
await authSession.login(loginUrl, locationUrl.href)
5162
}
5263

5364
async signup () {
@@ -59,14 +70,26 @@ export default class SolidAuth implements AuthContext {
5970
}
6071

6172
onSessionUpdated (callback: () => unknown) {
62-
authSession.events.on('login', callback)
63-
authSession.events.on('logout', callback)
64-
authSession.events.on('sessionRestore', callback)
73+
const sessionEventTarget = authSession as unknown as EventTarget
74+
const listener = () => {
75+
callback()
76+
}
77+
if (typeof sessionEventTarget.addEventListener === 'function') {
78+
sessionEventTarget.addEventListener('sessionStateChange', listener)
79+
} else {
80+
authSession.events.on('login', callback)
81+
authSession.events.on('logout', callback)
82+
authSession.events.on('sessionRestore', callback)
83+
}
6584

6685
return () => {
67-
authSession.events.off('login', callback)
68-
authSession.events.off('logout', callback)
69-
authSession.events.off('sessionRestore', callback)
86+
if (typeof sessionEventTarget.removeEventListener === 'function') {
87+
sessionEventTarget.removeEventListener('sessionStateChange', listener)
88+
} else {
89+
authSession.events.off('login', callback)
90+
authSession.events.off('logout', callback)
91+
authSession.events.off('sessionRestore', callback)
92+
}
7093
}
7194
}
7295
}

src/login/login.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ export async function ensureLoadedPreferences (
162162
} else {
163163
throw new Error(`(via loadPrefs) ${err}`)
164164
}
165+
166+
context.preferencesFileError = m2
165167
}
166168
return context
167169
}
@@ -513,10 +515,7 @@ export function renderSignInPopup (dom: HTMLDocument) {
513515
// Login
514516
const locationUrl = new URL(window.location.href)
515517
locationUrl.hash = '' // remove hash part
516-
await authSession.login({
517-
redirectUrl: locationUrl.href,
518-
oidcIssuer: issuerUri
519-
})
518+
await authSession.login(issuerUri, locationUrl.href)
520519
} catch (err) {
521520
alert(err.message)
522521
}
@@ -669,9 +668,9 @@ export function loginStatusBox (
669668
}
670669

671670
box.refresh = function () {
672-
const sessionInfo = authSession.info
673-
if (sessionInfo && sessionInfo.webId && sessionInfo.isLoggedIn) {
674-
me = solidLogicSingleton.store.sym(sessionInfo.webId)
671+
const webId = authSession.webId
672+
if (webId) {
673+
me = solidLogicSingleton.store.sym(webId)
675674
} else {
676675
me = null
677676
}
@@ -716,6 +715,12 @@ authSession.events.on('logout', async () => {
716715
await fetch(openidConfiguration.end_session_endpoint, { credentials: 'include' })
717716
}
718717
}
718+
719+
try {
720+
await fetch('/.well-known/solid/logout', { credentials: 'include' })
721+
} catch (_err) {
722+
// Not all deployments expose NSS-compatible well-known logout endpoint.
723+
}
719724
} catch (_err) {
720725
// Do nothing
721726
}
@@ -1047,22 +1052,10 @@ export function newAppInstance (
10471052
* and/or a developer
10481053
*/
10491054
export async function getUserRoles (): Promise<Array<NamedNode>> {
1050-
const sessionInfo = authSession.info
1051-
if (!sessionInfo?.isLoggedIn || !sessionInfo?.webId) {
1052-
return []
1053-
}
1054-
1055-
const currentUser = authn.currentUser()
1056-
if (!currentUser) {
1057-
return []
1058-
}
1059-
10601055
try {
1061-
const { me, preferencesFile, preferencesFileError } = await ensureLoadedPreferences({
1062-
me: currentUser
1063-
})
1056+
const { me, preferencesFile, preferencesFileError } = await ensureLoadedPreferences({})
10641057
if (!preferencesFile || preferencesFileError) {
1065-
throw new Error(preferencesFileError || 'Unable to load user preferences file.')
1058+
throw new Error(preferencesFileError)
10661059
}
10671060
return solidLogicSingleton.store.each(
10681061
me,

src/v2/components/auth/loginButton/LoginButton.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,7 @@ export class LoginButton extends LitElement {
419419

420420
const locationUrl = new URL(window.location.href)
421421
locationUrl.hash = ''
422-
await authSession.login({
423-
redirectUrl: locationUrl.href,
424-
oidcIssuer: issuerUri
425-
})
422+
await authSession.login(issuerUri, locationUrl.href)
426423
} catch (err: any) {
427424
this._errorMsg = err.message || String(err)
428425
this.requestUpdate()

src/v2/components/layout/footer/Footer.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,6 @@ export class Footer extends LitElement {
107107
if (typeof authSession.events.off === 'function') {
108108
authSession.events.off('login', this._updateFooter)
109109
authSession.events.off('logout', this._updateFooter)
110-
} else if (typeof authSession.events.removeListener === 'function') {
111-
authSession.events.removeListener('login', this._updateFooter)
112-
authSession.events.removeListener('logout', this._updateFooter)
113110
}
114111
super.disconnectedCallback()
115112
}

src/v2/components/layout/header/Header.ts

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LitElement, html, css } from 'lit'
22
import { icons } from '../../../../iconBase'
3-
import { authSession } from 'solid-logic'
3+
import { authSession, authn, performServerSideLogout } from 'solid-logic'
44
import '../../auth/loginButton/index'
55
import '../../auth/signupButton/index'
66
import { ifDefined } from 'lit/directives/if-defined.js'
@@ -14,6 +14,36 @@ const DEFAULT_SOLID_ICON_URL = 'https://solidproject.org/assets/img/solid-emblem
1414
const DEFAULT_SIGNUP_URL = 'https://solidproject.org/get_a_pod'
1515
const DEFAULT_LOGGEDIN_MENU_BUTTON_AVATAR = icons.iconBase + 'emptyProfileAvatar.png'
1616

17+
async function clearPersistedAuthState (): Promise<void> {
18+
if (typeof window === 'undefined') {
19+
return
20+
}
21+
22+
const explicitKeys = ['loginIssuer', 'preLoginRedirectHash']
23+
for (const key of explicitKeys) {
24+
window.localStorage.removeItem(key)
25+
window.sessionStorage.removeItem(key)
26+
}
27+
28+
if (typeof indexedDB === 'undefined') {
29+
return
30+
}
31+
32+
const databases = ['soidc', 'solid-client-authn-store', 'solid-client-authn']
33+
for (const dbName of databases) {
34+
await new Promise<void>((resolve) => {
35+
try {
36+
const request = indexedDB.deleteDatabase(dbName)
37+
request.onsuccess = () => resolve()
38+
request.onerror = () => resolve()
39+
request.onblocked = () => resolve()
40+
} catch (_err) {
41+
resolve()
42+
}
43+
})
44+
}
45+
}
46+
1747
export type HeaderAuthState = 'logged-out' | 'logged-in'
1848

1949
export type HeaderMenuItem = {
@@ -51,7 +81,8 @@ export class Header extends LitElement {
5181
accountMenuOpen: { state: true },
5282
helpMenuOpen: { state: true },
5383
hasSlottedAccountMenu: { state: true },
54-
hasSlottedHelpMenu: { state: true }
84+
hasSlottedHelpMenu: { state: true },
85+
authResolved: { state: true }
5586
}
5687

5788
static styles = css`
@@ -514,6 +545,14 @@ export class Header extends LitElement {
514545
declare helpMenuOpen: boolean
515546
declare hasSlottedAccountMenu: boolean
516547
declare hasSlottedHelpMenu: boolean
548+
declare authResolved: boolean
549+
private _refreshPromise: Promise<void> | null = null
550+
551+
private readonly handleAuthSessionChange = () => {
552+
this.refreshAuthStateFromSession().catch(() => {
553+
// Keep auth event handling resilient on transient refresh failures.
554+
})
555+
}
517556

518557
constructor () {
519558
super()
@@ -538,20 +577,59 @@ export class Header extends LitElement {
538577
this.helpMenuOpen = false
539578
this.hasSlottedAccountMenu = false
540579
this.hasSlottedHelpMenu = false
580+
this.authResolved = false
541581
}
542582

543583
connectedCallback () {
544584
super.connectedCallback()
545585
document.addEventListener('click', this.handleDocumentClick)
546586
window.addEventListener('keydown', this.handleWindowKeydown)
587+
if (typeof authSession.events?.on === 'function') {
588+
authSession.events.on('login', this.handleAuthSessionChange)
589+
authSession.events.on('logout', this.handleAuthSessionChange)
590+
authSession.events.on('sessionRestore', this.handleAuthSessionChange)
591+
}
592+
this.refreshAuthStateFromSession().catch(() => {
593+
// Keep initial header render resilient on transient refresh failures.
594+
})
547595
}
548596

549597
disconnectedCallback () {
550598
document.removeEventListener('click', this.handleDocumentClick)
551599
window.removeEventListener('keydown', this.handleWindowKeydown)
600+
if (typeof authSession.events?.off === 'function') {
601+
authSession.events.off('login', this.handleAuthSessionChange)
602+
authSession.events.off('logout', this.handleAuthSessionChange)
603+
authSession.events.off('sessionRestore', this.handleAuthSessionChange)
604+
}
552605
super.disconnectedCallback()
553606
}
554607

608+
private async refreshAuthStateFromSession () {
609+
if (!this._refreshPromise) {
610+
this._refreshPromise = (async () => {
611+
try {
612+
await authn.checkUser()
613+
// Some auth stacks resolve session state asynchronously after first check.
614+
if (!authn.currentUser()) {
615+
await authn.checkUser()
616+
}
617+
} catch (_err) {
618+
// Keep rendering even if session refresh cannot complete.
619+
}
620+
})()
621+
}
622+
623+
try {
624+
await this._refreshPromise
625+
} finally {
626+
this._refreshPromise = null
627+
}
628+
629+
this.authState = authn.currentUser() ? 'logged-in' : 'logged-out'
630+
this.authResolved = true
631+
}
632+
555633
private handleHelpMenuClick (item: HeaderMenuItem, event: MouseEvent) {
556634
event.preventDefault()
557635
this.helpMenuOpen = false
@@ -669,8 +747,8 @@ export class Header extends LitElement {
669747
`
670748
}
671749

672-
private handleLoginSuccess () {
673-
this.authState = 'logged-in'
750+
private async handleLoginSuccess () {
751+
await this.refreshAuthStateFromSession()
674752
this.dispatchEvent(new CustomEvent('auth-action-select', {
675753
detail: { role: 'login' },
676754
bubbles: true,
@@ -680,12 +758,25 @@ export class Header extends LitElement {
680758

681759
private async handleLogout () {
682760
this.accountMenuOpen = false
761+
const issuer = window.localStorage.getItem('loginIssuer') || ''
762+
683763
try {
684764
await authSession.logout()
685765
} catch (_err) {
686766
// logout errors are non-fatal — proceed to clear state
687767
}
688-
this.authState = 'logged-out'
768+
769+
await clearPersistedAuthState()
770+
771+
const redirectedToServerLogout = await performServerSideLogout({
772+
issuer,
773+
postLogoutRedirectPath: '/'
774+
})
775+
if (redirectedToServerLogout) {
776+
return
777+
}
778+
779+
await this.refreshAuthStateFromSession()
689780
this.dispatchEvent(new CustomEvent('logout-select', {
690781
detail: { role: 'logout' },
691782
bubbles: true,
@@ -802,6 +893,10 @@ export class Header extends LitElement {
802893
`
803894
}
804895

896+
if (!this.authResolved) {
897+
return html`<div class="auth-actions" part="auth-actions"></div>`
898+
}
899+
805900
if (this.authState === 'logged-out') {
806901
return this.renderLoggedOutActions()
807902
}

0 commit comments

Comments
 (0)