OIDC: Skip nonce for mobile flow to fix app login

Some IdPs (e.g. Authentik) don't echo the nonce in the id_token for
the authorization code flow, causing "nonce mismatch, got: undefined"
errors when the mobile app attempts SSO login. The mobile flow already
uses PKCE which provides equivalent replay protection, so nonce is not
needed. Web flow continues to use nonce for defense-in-depth.
This commit is contained in:
Denis Arnst 2026-02-13 12:31:31 +01:00
parent 67f8eb6815
commit a6848065e1
No known key found for this signature in database
GPG key ID: D5866C58940197BF
2 changed files with 7 additions and 7 deletions

View file

@ -107,7 +107,6 @@ class OidcAuthStrategy {
this.openIdAuthSession.delete(state) this.openIdAuthSession.delete(state)
sessionData = { sessionData = {
state: state, state: state,
nonce: mobileSession.nonce,
sso_redirect_uri: mobileSession.sso_redirect_uri sso_redirect_uri: mobileSession.sso_redirect_uri
} }
isMobileCallback = true isMobileCallback = true
@ -434,7 +433,9 @@ class OidcAuthStrategy {
} }
// Generate nonce to bind id_token to this session (OIDC Core 3.1.2.1) // Generate nonce to bind id_token to this session (OIDC Core 3.1.2.1)
const nonce = OpenIDClient.generators.nonce() // Nonce is only used for web flow. Mobile flow relies on PKCE for replay protection,
// and some IdPs don't echo the nonce in the id_token for authorization code flow.
const nonce = isMobileFlow ? undefined : OpenIDClient.generators.nonce()
if (isMobileFlow) { if (isMobileFlow) {
// For mobile: store session data in the openIdAuthSession Map (keyed by state) // For mobile: store session data in the openIdAuthSession Map (keyed by state)
@ -442,7 +443,6 @@ class OidcAuthStrategy {
this.openIdAuthSession.set(state, { this.openIdAuthSession.set(state, {
mobile_redirect_uri: req.query.redirect_uri, mobile_redirect_uri: req.query.redirect_uri,
sso_redirect_uri: redirectUri, sso_redirect_uri: redirectUri,
nonce: nonce,
created_at: Date.now() created_at: Date.now()
}) })
} }

View file

@ -693,8 +693,8 @@ describe('OidcAuthStrategy', function () {
sinon.stub(strategy, 'verifyUser').resolves(mockUser) sinon.stub(strategy, 'verifyUser').resolves(mockUser)
// Pre-populate Map as if getAuthorizationUrl stored mobile session // Pre-populate Map as if getAuthorizationUrl stored mobile session
// Note: mobile flow does not use nonce (relies on PKCE instead)
strategy.openIdAuthSession.set('mobile-state', { strategy.openIdAuthSession.set('mobile-state', {
nonce: 'mobile-nonce',
sso_redirect_uri: 'http://localhost/auth/openid/mobile-redirect', sso_redirect_uri: 'http://localhost/auth/openid/mobile-redirect',
mobile_redirect_uri: 'audiobookshelf://oauth' mobile_redirect_uri: 'audiobookshelf://oauth'
}) })
@ -711,9 +711,9 @@ describe('OidcAuthStrategy', function () {
// Should delete the Map entry after use // Should delete the Map entry after use
expect(strategy.openIdAuthSession.has('mobile-state')).to.be.false expect(strategy.openIdAuthSession.has('mobile-state')).to.be.false
// Should use mobile nonce and code_verifier from query // Should use code_verifier from query; nonce is undefined for mobile flow
const [, , checks] = mockClient.callback.firstCall.args const [, , checks] = mockClient.callback.firstCall.args
expect(checks.nonce).to.equal('mobile-nonce') expect(checks.nonce).to.be.undefined
expect(checks.code_verifier).to.equal('mobile-verifier') expect(checks.code_verifier).to.equal('mobile-verifier')
}) })
@ -965,7 +965,7 @@ describe('OidcAuthStrategy', function () {
expect(strategy.openIdAuthSession.has('mob-state')).to.be.true expect(strategy.openIdAuthSession.has('mob-state')).to.be.true
const stored = strategy.openIdAuthSession.get('mob-state') const stored = strategy.openIdAuthSession.get('mob-state')
expect(stored.mobile_redirect_uri).to.equal('audiobookshelf://oauth') expect(stored.mobile_redirect_uri).to.equal('audiobookshelf://oauth')
expect(stored.nonce).to.equal('mock-nonce') expect(stored.nonce).to.be.undefined
expect(stored.sso_redirect_uri).to.include('/auth/openid/mobile-redirect') expect(stored.sso_redirect_uri).to.include('/auth/openid/mobile-redirect')
}) })