Skip to content

Commit 1626c13

Browse files
committed
Fix multiple issues
* Show a Logout button as necessary when credentials have gone stale. * Make login/logout buttons more sensible sizes. * Fix audiobook fulfillment with bearer tokens. * Improve handling of audiobook fulfillment credentials in general. Affects: https://ebce-lyrasis.atlassian.net/browse/PP-3614 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-3641
1 parent b634b06 commit 1626c13

22 files changed

Lines changed: 672 additions & 309 deletions

File tree

README-CHANGES.xml

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,13 +1012,43 @@
10121012
</c:change>
10131013
</c:changes>
10141014
</c:release>
1015-
<c:release date="2026-02-03T13:29:02+00:00" is-open="true" ticket-system="org.lyrasis.jira" version="1.25.0">
1015+
<c:release date="2026-02-04T15:44:37+00:00" is-open="true" ticket-system="org.lyrasis.jira" version="1.25.0">
10161016
<c:changes>
1017-
<c:change date="2026-02-03T13:29:02+00:00" summary="Attempt to allow notifications on Android 10.">
1017+
<c:change date="2026-02-03T00:00:00+00:00" summary="Attempt to allow notifications on Android 10.">
10181018
<c:tickets>
10191019
<c:ticket id="PP-3631"/>
10201020
</c:tickets>
10211021
</c:change>
1022+
<c:change date="2026-01-26T00:00:00+00:00" summary="Large refactoring for SAML and credential expiration.">
1023+
<c:tickets>
1024+
<c:ticket id="PP-3495"/>
1025+
</c:tickets>
1026+
</c:change>
1027+
<c:change date="2026-01-27T00:00:00+00:00" summary="Fix a very unlikely crash when refreshing an OPDS feed after an error.">
1028+
<c:tickets>
1029+
<c:ticket id="PP-3576"/>
1030+
</c:tickets>
1031+
</c:change>
1032+
<c:change date="2026-01-28T00:00:00+00:00" summary="Update catalog language to change Reservations to Holds.">
1033+
<c:tickets>
1034+
<c:ticket id="PP-3581"/>
1035+
</c:tickets>
1036+
</c:change>
1037+
<c:change date="2026-01-30T00:00:00+00:00" summary="Correct a possible ANR.">
1038+
<c:tickets>
1039+
<c:ticket id="PP-3577"/>
1040+
</c:tickets>
1041+
</c:change>
1042+
<c:change date="2026-02-04T00:00:00+00:00" summary="Allow a Logout button when SAML credentials have expired.">
1043+
<c:tickets>
1044+
<c:ticket id="PP-3614"/>
1045+
</c:tickets>
1046+
</c:change>
1047+
<c:change date="2026-02-04T15:44:37+00:00" summary="Correct audiobook download failures.">
1048+
<c:tickets>
1049+
<c:ticket id="PP-3641"/>
1050+
</c:tickets>
1051+
</c:change>
10221052
</c:changes>
10231053
</c:release>
10241054
</c:releases>

palace-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestRequest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import org.librarysimplified.audiobook.license_check.spi.SingleLicenseCheckProvi
66
import org.librarysimplified.audiobook.manifest.api.PlayerPalaceID
77
import org.librarysimplified.audiobook.manifest_fulfill.api.ManifestFulfillmentStrategies
88
import org.librarysimplified.audiobook.manifest_fulfill.api.ManifestFulfillmentStrategyRegistryType
9+
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsType
910
import org.librarysimplified.audiobook.manifest_fulfill.spi.ManifestFulfilled
1011
import org.librarysimplified.audiobook.manifest_parser.api.ManifestParsers
1112
import org.librarysimplified.audiobook.manifest_parser.api.ManifestParsersType
1213
import org.librarysimplified.audiobook.manifest_parser.extension_spi.ManifestParserExtensionType
1314
import org.librarysimplified.http.api.LSHTTPClientType
1415
import org.librarysimplified.http.api.LSHTTPProblemReportParserFactoryType
1516
import org.librarysimplified.services.api.ServiceDirectoryType
16-
import org.nypl.simplified.accounts.api.AccountAuthenticationCredentials
1717
import java.io.File
1818
import java.io.IOException
1919
import java.util.ServiceLoader
@@ -63,7 +63,7 @@ data class AudioBookManifestRequest(
6363
* The credentials used for license and manifest requests.
6464
*/
6565

66-
val credentials: AccountAuthenticationCredentials?,
66+
val credentials: ManifestFulfillmentCredentialsType?,
6767

6868
/**
6969
* A service directory used to locate any required application services.

palace-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookStrategy.kt

Lines changed: 35 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmen
1717
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsToken
1818
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsType
1919
import org.librarysimplified.audiobook.manifest_fulfill.opa.OPAManifestFulfillmentStrategyProviderType
20-
import org.librarysimplified.audiobook.manifest_fulfill.opa.OPAManifestURI
20+
import org.librarysimplified.audiobook.manifest_fulfill.opa.OPAManifestURI.Indirect
2121
import org.librarysimplified.audiobook.manifest_fulfill.opa.OPAParameters
2222
import org.librarysimplified.audiobook.manifest_fulfill.opa.OPAPassword
23+
import org.librarysimplified.audiobook.manifest_fulfill.opa.OPAPassword.Password
2324
import org.librarysimplified.audiobook.manifest_fulfill.spi.ManifestFulfilled
2425
import org.librarysimplified.audiobook.manifest_fulfill.spi.ManifestFulfillmentError
2526
import org.librarysimplified.audiobook.manifest_fulfill.spi.ManifestFulfillmentEvent
@@ -28,6 +29,8 @@ import org.librarysimplified.audiobook.manifest_parser.api.ManifestUnparsed
2829
import org.librarysimplified.audiobook.parser.api.ParseError
2930
import org.librarysimplified.audiobook.parser.api.ParseResult
3031
import org.librarysimplified.audiobook.parser.api.ParseWarning
32+
import org.librarysimplified.http.api.LSHTTPAuthorizationBasic
33+
import org.librarysimplified.http.api.LSHTTPAuthorizationBearerToken
3134
import org.librarysimplified.http.api.LSHTTPAuthorizationType
3235
import org.librarysimplified.http.api.LSHTTPClientType
3336
import org.librarysimplified.http.api.LSHTTPProblemReport
@@ -38,8 +41,6 @@ import org.librarysimplified.http.downloads.LSHTTPDownloadState.LSHTTPDownloadRe
3841
import org.librarysimplified.http.downloads.LSHTTPDownloadState.LSHTTPDownloadResult.DownloadFailed.DownloadFailedServer
3942
import org.librarysimplified.http.downloads.LSHTTPDownloadState.LSHTTPDownloadResult.DownloadFailed.DownloadFailedUnacceptableMIME
4043
import org.librarysimplified.http.downloads.LSHTTPDownloads
41-
import org.nypl.simplified.accounts.api.AccountAuthenticatedHTTP.createAuthorizationIfPresent
42-
import org.nypl.simplified.accounts.api.AccountAuthenticationCredentials
4344
import org.nypl.simplified.books.book_database.api.BookFormats
4445
import org.nypl.simplified.taskrecorder.api.TaskRecorder
4546
import org.nypl.simplified.taskrecorder.api.TaskRecorderType
@@ -192,7 +193,7 @@ class AudioBookStrategy(
192193

193194
val httpRequest =
194195
this.request.httpClient.newRequest(target.target)
195-
.setAuthorization(createAuthorizationIfPresent(this.request.credentials))
196+
.setAuthorization(authorization = createAuthorization(this.request.credentials))
196197
.build()
197198

198199
val temporaryFile =
@@ -282,6 +283,25 @@ class AudioBookStrategy(
282283
}
283284
}
284285

286+
private fun createAuthorization(
287+
credentials: ManifestFulfillmentCredentialsType?
288+
): LSHTTPAuthorizationType? {
289+
return when (credentials) {
290+
is ManifestFulfillmentCredentialsBasic -> {
291+
LSHTTPAuthorizationBasic.ofUsernamePassword(
292+
userName = credentials.userName,
293+
password = credentials.password
294+
)
295+
}
296+
is ManifestFulfillmentCredentialsToken -> {
297+
LSHTTPAuthorizationBearerToken.ofToken(
298+
token = credentials.token
299+
)
300+
}
301+
null -> null
302+
}
303+
}
304+
285305
private fun recordProblemReport(
286306
report: LSHTTPProblemReport?
287307
) {
@@ -296,38 +316,9 @@ class AudioBookStrategy(
296316
): TaskResult<AudioBookManifestData> {
297317
this.taskRecorder.beginNewStep("Retrieving manifest via LCP license.")
298318

299-
val credentials: ManifestFulfillmentCredentialsType? =
300-
when (val c = this.request.credentials) {
301-
is AccountAuthenticationCredentials.Basic -> {
302-
ManifestFulfillmentCredentialsBasic(
303-
userName = c.userName.value,
304-
password = c.password.value
305-
)
306-
}
307-
308-
is AccountAuthenticationCredentials.BasicToken -> {
309-
ManifestFulfillmentCredentialsBasic(
310-
userName = c.userName.value,
311-
password = c.password.value
312-
)
313-
}
314-
315-
is AccountAuthenticationCredentials.OAuthWithIntermediary -> {
316-
ManifestFulfillmentCredentialsToken(c.accessToken)
317-
}
318-
319-
is AccountAuthenticationCredentials.SAML2_0 -> {
320-
ManifestFulfillmentCredentialsToken(c.accessToken)
321-
}
322-
323-
null -> {
324-
null
325-
}
326-
}
327-
328319
return when (val result = LCPDownloads.downloadManifestFromPublication(
329320
context = this.context,
330-
credentials = credentials,
321+
credentials = request.credentials,
331322
license = license,
332323
receiver = { event ->
333324
this.logger.debug("Downloading manifest event: {}", event)
@@ -486,59 +477,35 @@ class AudioBookStrategy(
486477

487478
val parameters =
488479
when (val credentials = this.request.credentials) {
489-
is AccountAuthenticationCredentials.Basic -> {
490-
val password = credentials.password.value
491-
val opaPassword = if (password.isBlank()) {
492-
OPAPassword.NotRequired
493-
} else {
494-
OPAPassword.Password(password)
495-
}
496-
497-
OPAParameters(
498-
userName = credentials.userName.value,
499-
password = opaPassword,
500-
clientKey = secretService.clientKey.orEmpty(),
501-
clientPass = secretService.clientPass.orEmpty(),
502-
targetURI = OPAManifestURI.Indirect(targetURI),
503-
userAgent = this.request.userAgent
480+
null -> {
481+
throw UnsupportedOperationException(
482+
"Credentials are required for Overdrive fulfillment"
504483
)
505484
}
506485

507-
is AccountAuthenticationCredentials.BasicToken -> {
508-
val password = credentials.password.value
486+
is ManifestFulfillmentCredentialsBasic -> {
487+
val password = credentials.password
509488
val opaPassword = if (password.isBlank()) {
510489
OPAPassword.NotRequired
511490
} else {
512-
OPAPassword.Password(password)
491+
Password(password)
513492
}
514493

515494
OPAParameters(
516-
userName = credentials.userName.value,
495+
userName = credentials.userName,
517496
password = opaPassword,
518497
clientKey = secretService.clientKey.orEmpty(),
519498
clientPass = secretService.clientPass.orEmpty(),
520-
targetURI = OPAManifestURI.Indirect(targetURI),
499+
targetURI = Indirect(targetURI),
521500
userAgent = this.request.userAgent
522501
)
523502
}
524503

525-
is AccountAuthenticationCredentials.OAuthWithIntermediary -> {
526-
throw UnsupportedOperationException(
527-
"Can't use bearer tokens for Overdrive fulfillment"
528-
)
529-
}
530-
531-
is AccountAuthenticationCredentials.SAML2_0 -> {
504+
is ManifestFulfillmentCredentialsToken -> {
532505
throw UnsupportedOperationException(
533506
"Can't use bearer tokens for Overdrive fulfillment"
534507
)
535508
}
536-
537-
null -> {
538-
throw UnsupportedOperationException(
539-
"Credentials are required for Overdrive fulfillment"
540-
)
541-
}
542509
}
543510

544511
strategies.create(parameters)
@@ -553,31 +520,7 @@ class AudioBookStrategy(
553520
val parameters =
554521
ManifestFulfillmentBasicParameters(
555522
uri = targetURI,
556-
credentials = when (val c = this.request.credentials) {
557-
is AccountAuthenticationCredentials.Basic -> {
558-
ManifestFulfillmentCredentialsBasic(
559-
userName = c.userName.value,
560-
password = c.password.value
561-
)
562-
}
563-
564-
is AccountAuthenticationCredentials.BasicToken -> {
565-
ManifestFulfillmentCredentialsBasic(
566-
userName = c.userName.value,
567-
password = c.password.value
568-
)
569-
}
570-
571-
is AccountAuthenticationCredentials.OAuthWithIntermediary -> {
572-
ManifestFulfillmentCredentialsToken(c.accessToken)
573-
}
574-
575-
is AccountAuthenticationCredentials.SAML2_0 -> {
576-
ManifestFulfillmentCredentialsToken(c.accessToken)
577-
}
578-
579-
null -> null
580-
},
523+
credentials = this.request.credentials,
581524
httpClient = this.request.services.requireService(LSHTTPClientType::class.java),
582525
userAgent = this.request.userAgent
583526
)

palace-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/BorrowContextType.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package org.nypl.simplified.books.borrowing
22

33
import android.app.Application
44
import org.joda.time.Instant
5+
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsBasic
6+
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsToken
7+
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsType
58
import org.librarysimplified.http.api.LSHTTPClientType
69
import org.librarysimplified.services.api.ServiceDirectoryType
710
import org.nypl.drm.core.AdobeAdeptExecutorType
@@ -303,6 +306,7 @@ interface BorrowContextType {
303306
BorrowSubtaskCredentials.UseAccountCredentials -> {
304307
this.account.loginState.credentials
305308
}
309+
306310
is BorrowSubtaskCredentials.UseBearerToken -> {
307311
this.taskRecorder.currentStepFailed(
308312
message = "A previous subtask required the use of a bearer token, but those cannot be used for this subtask.",
@@ -314,6 +318,44 @@ interface BorrowContextType {
314318
}
315319
}
316320

321+
/**
322+
* Equivalent to [takeSubtaskCredentials] but the result is transformed into a form useful
323+
* for fulfilling audiobook manifests and licenses.
324+
*/
325+
326+
fun takeSubtaskCredentialsForAudiobook(): ManifestFulfillmentCredentialsType? {
327+
return when (val c = this.takeSubtaskCredentials()) {
328+
BorrowSubtaskCredentials.UseAccountCredentials -> {
329+
when (val ac = this.account.loginState.credentials) {
330+
is AccountAuthenticationCredentials.Basic -> {
331+
ManifestFulfillmentCredentialsBasic(
332+
userName = ac.userName.value,
333+
password = ac.password.value
334+
)
335+
}
336+
337+
is AccountAuthenticationCredentials.BasicToken -> {
338+
ManifestFulfillmentCredentialsToken(ac.authenticationTokenInfo.accessToken)
339+
}
340+
341+
is AccountAuthenticationCredentials.OAuthWithIntermediary -> {
342+
throw UnsupportedOperationException("No OAuth audiobook support.")
343+
}
344+
345+
is AccountAuthenticationCredentials.SAML2_0 -> {
346+
ManifestFulfillmentCredentialsToken(ac.accessToken)
347+
}
348+
349+
null -> null
350+
}
351+
}
352+
353+
is BorrowSubtaskCredentials.UseBearerToken -> {
354+
ManifestFulfillmentCredentialsToken(c.token)
355+
}
356+
}
357+
}
358+
317359
/**
318360
* Information about the current SAML download, if one is in progress.
319361
*/

palace-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import one.irradia.mime.api.MIMECompatibility
55
import one.irradia.mime.api.MIMEType
66
import org.librarysimplified.audiobook.api.PlayerUserAgent
77
import org.librarysimplified.audiobook.manifest.api.PlayerPalaceID
8+
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsType
89
import org.nypl.simplified.accounts.api.AccountReadableType
910
import org.nypl.simplified.books.audio.AudioBookLink
1011
import org.nypl.simplified.books.audio.AudioBookManifestRequest
@@ -87,8 +88,8 @@ class BorrowAudioBook private constructor() : BorrowSubtaskType {
8788
): DownloadedManifest {
8889
context.taskRecorder.beginNewStep("Executing audio book manifest strategy...")
8990

90-
val credentials =
91-
context.takeSubtaskCredentialsRequiringAccount()
91+
val credentials: ManifestFulfillmentCredentialsType? =
92+
context.takeSubtaskCredentialsForAudiobook()
9293

9394
val strategy =
9495
context.audioBookManifestStrategies.createStrategy(

palace-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowLCPAudiobook.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import one.irradia.mime.api.MIMECompatibility
55
import one.irradia.mime.api.MIMEType
66
import org.librarysimplified.audiobook.api.PlayerUserAgent
77
import org.librarysimplified.audiobook.manifest.api.PlayerPalaceID
8+
import org.librarysimplified.audiobook.manifest_fulfill.basic.ManifestFulfillmentCredentialsType
89
import org.nypl.simplified.accounts.api.AccountReadableType
910
import org.nypl.simplified.books.api.BookDRMKind
1011
import org.nypl.simplified.books.audio.AudioBookLink
@@ -83,13 +84,16 @@ class BorrowLCPAudiobook : BorrowSubtaskType {
8384
): DownloadedManifest {
8485
context.taskRecorder.beginNewStep("Executing audio book manifest strategy...")
8586

87+
val credentials: ManifestFulfillmentCredentialsType? =
88+
context.takeSubtaskCredentialsForAudiobook()
89+
8690
val strategy =
8791
context.audioBookManifestStrategies.createStrategy(
8892
context = context.application,
8993
AudioBookManifestRequest(
9094
cacheDirectory = context.cacheDirectory(),
9195
contentType = context.currentAcquisitionPathElement.mimeType,
92-
credentials = context.account.loginState.credentials,
96+
credentials = credentials,
9397
httpClient = context.httpClient,
9498
services = context.services,
9599
target = AudioBookLink.License(context.currentURICheck()),

palace-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileAccountLoginTask.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,12 @@ class ProfileAccountLoginTask(
227227
return when (val loginState = this.account.loginState) {
228228
is AccountLoggingIn,
229229
is AccountLoggingInWaitingForExternalAuthentication -> {
230-
this.account.setLoginState(AccountNotLoggedIn(loginState.credentials))
230+
val previous = loginState.previousCredentials
231+
if (previous != null) {
232+
this.account.setLoginState(AccountLoggedInStaleCredentials(credentials = previous))
233+
} else {
234+
this.account.setLoginState(AccountNotLoggedIn(loginState.credentials))
235+
}
231236
this.steps.finishSuccess(Unit)
232237
}
233238

0 commit comments

Comments
 (0)