Skip to content

Commit 1d0ec97

Browse files
authored
Merge pull request #745 from tunjid/bugfix/1.1.4
Bugfix/1.1.4
2 parents c8c1df5 + 6b825ff commit 1d0ec97

File tree

3 files changed

+76
-46
lines changed

3 files changed

+76
-46
lines changed

data/core/src/commonMain/kotlin/com/tunjid/heron/data/network/SessionManager.kt

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@ import com.atproto.server.RefreshSessionResponse
2222
import com.tunjid.heron.data.core.models.OauthUriRequest
2323
import com.tunjid.heron.data.core.models.Server
2424
import com.tunjid.heron.data.core.models.SessionRequest
25-
import com.tunjid.heron.data.core.types.GenericUri
26-
import com.tunjid.heron.data.core.types.ProfileHandle
2725
import com.tunjid.heron.data.core.types.ProfileId
2826
import com.tunjid.heron.data.lexicons.XrpcBlueskyApi
2927
import com.tunjid.heron.data.lexicons.XrpcSerializersModule
3028
import com.tunjid.heron.data.network.oauth.DpopKeyPair
3129
import com.tunjid.heron.data.network.oauth.OAuthApi
32-
import com.tunjid.heron.data.network.oauth.OAuthAuthorizationRequest
3330
import com.tunjid.heron.data.network.oauth.OAuthClient
3431
import com.tunjid.heron.data.network.oauth.OAuthScope
3532
import com.tunjid.heron.data.network.oauth.OAuthToken
@@ -65,6 +62,7 @@ import io.ktor.http.encodedPath
6562
import io.ktor.http.isSuccess
6663
import io.ktor.http.set
6764
import io.ktor.http.takeFrom
65+
import kotlin.time.Clock
6866
import kotlin.time.Duration.Companion.seconds
6967
import kotlinx.coroutines.flow.MutableStateFlow
7068
import kotlinx.coroutines.flow.first
@@ -77,9 +75,9 @@ import sh.christian.ozone.api.runtime.buildXrpcJsonConfiguration
7775

7876
internal interface SessionManager {
7977

80-
suspend fun startOauthSessionUri(
78+
suspend fun initiateOauthSession(
8179
request: OauthUriRequest,
82-
): GenericUri
80+
): SavedState.AuthTokens.Pending
8381

8482
suspend fun createSession(
8583
request: SessionRequest,
@@ -125,25 +123,26 @@ internal class PersistedSessionManager @Inject constructor(
125123
client = authHttpClient,
126124
)
127125

128-
private var pendingOauthSession: OauthSession? = null
129-
130-
override suspend fun startOauthSessionUri(
126+
override suspend fun initiateOauthSession(
131127
request: OauthUriRequest,
132-
): GenericUri {
128+
): SavedState.AuthTokens.Pending {
133129
sessionRequestUrl.update { Url(request.server.endpoint) }
134130
return oAuthApi.buildAuthorizationRequest(
135131
oauthClient = HeronOauthClient,
136132
scopes = HeronOauthScopes,
137133
loginHandleHint = request.handle.id,
138134
)
139-
.also {
140-
pendingOauthSession = OauthSession(
141-
handle = request.handle,
142-
request = it,
135+
.let {
136+
SavedState.AuthTokens.Pending.DPoP(
137+
profileHandle = request.handle,
138+
endpoint = request.server.endpoint,
139+
authorizeRequestUrl = it.authorizeRequestUrl,
140+
codeVerifier = it.codeVerifier,
141+
nonce = it.nonce,
142+
state = it.state,
143+
expiresAt = Clock.System.now() + it.expiresIn,
143144
)
144145
}
145-
.authorizeRequestUrl
146-
.let(::GenericUri)
147146
}
148147

149148
override suspend fun createSession(
@@ -171,36 +170,44 @@ internal class PersistedSessionManager @Inject constructor(
171170
}
172171
.requireResponse()
173172
is SessionRequest.Oauth -> {
174-
val pendingRequest = pendingOauthSession
175-
?: throw IllegalStateException("Expired authentication session")
173+
val existingAuth = savedStateDataSource.savedState.value.auth
174+
val pendingRequest = existingAuth as? SavedState.AuthTokens.Pending.DPoP
175+
?: throw IllegalStateException("No pending oauth session to finalize. Current auth state: $existingAuth")
176176

177-
try {
178-
val callbackUrl = Url(request.callbackUri.uri)
177+
require(request.server.endpoint == pendingRequest.endpoint) {
178+
"Mismatched server endpoints in OAuth flow. Expected ${pendingRequest.endpoint}, but got ${request.server.endpoint}"
179+
}
179180

180-
val code = callbackUrl.parameters[OauthCallbackUriCodeParam]
181-
?: throw IllegalStateException("No auth code")
181+
val callbackUrl = Url(request.callbackUri.uri)
182182

183-
val oAuthToken = oAuthApi.requestToken(
184-
oauthClient = HeronOauthClient,
185-
nonce = pendingRequest.request.nonce,
186-
codeVerifier = pendingRequest.request.codeVerifier,
187-
code = code,
188-
)
183+
val state = callbackUrl.parameters["state"]
184+
?: throw IllegalStateException("No state in callback")
189185

190-
val callingDid = api.resolveHandle(
191-
ResolveHandleQueryParams(Handle(pendingRequest.handle.id)),
192-
)
193-
.requireResponse()
194-
.did
186+
require(state == pendingRequest.state) {
187+
"Mismatched state in OAuth callback. Expected ${pendingRequest.state}, but got $state"
188+
}
195189

196-
if (oAuthToken.subject != callingDid) {
197-
throw IllegalStateException("Invalid login session")
198-
}
190+
val code = callbackUrl.parameters[OauthCallbackUriCodeParam]
191+
?: throw IllegalStateException("No auth code")
199192

200-
oAuthToken.toAppToken(authEndpoint = request.server.endpoint)
201-
} finally {
202-
pendingOauthSession = null
193+
val oAuthToken = oAuthApi.requestToken(
194+
oauthClient = HeronOauthClient,
195+
nonce = pendingRequest.nonce,
196+
codeVerifier = pendingRequest.codeVerifier,
197+
code = code,
198+
)
199+
200+
val callingDid = api.resolveHandle(
201+
ResolveHandleQueryParams(Handle(pendingRequest.profileHandle.id)),
202+
)
203+
.requireResponse()
204+
.did
205+
206+
if (oAuthToken.subject != callingDid) {
207+
throw IllegalStateException("Invalid login session")
203208
}
209+
210+
oAuthToken.toAppToken(authEndpoint = request.server.endpoint)
204211
}
205212
is SessionRequest.Guest -> SavedState.AuthTokens.Guest(
206213
server = request.server,
@@ -220,6 +227,7 @@ internal class PersistedSessionManager @Inject constructor(
220227
keyPair = authTokens.toKeyPair(),
221228
)
222229
is SavedState.AuthTokens.Guest,
230+
is SavedState.AuthTokens.Pending,
223231
null,
224232
-> Unit
225233
}
@@ -502,6 +510,7 @@ private val SavedState.AuthTokens?.defaultUrl
502510
is SavedState.AuthTokens.Authenticated.Bearer -> authEndpoint
503511
is SavedState.AuthTokens.Authenticated.DPoP -> issuerEndpoint
504512
is SavedState.AuthTokens.Guest -> server.endpoint
513+
is SavedState.AuthTokens.Pending.DPoP -> endpoint
505514
null -> Server.BlueSky.endpoint
506515
}
507516

@@ -511,11 +520,6 @@ private val SavedState.AuthTokens.Authenticated.singleAccessKey
511520
is SavedState.AuthTokens.Authenticated.DPoP -> "$auth-$refresh"
512521
}
513522

514-
private class OauthSession(
515-
val handle: ProfileHandle,
516-
val request: OAuthAuthorizationRequest,
517-
)
518-
519523
internal val BlueskyJson: Json = Json(
520524
from = buildXrpcJsonConfiguration(XrpcSerializersModule),
521525
builderAction = {

data/core/src/commonMain/kotlin/com/tunjid/heron/data/repository/AuthRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,13 @@ internal class AuthTokenRepository(
109109
override suspend fun oauthRequestUri(
110110
request: OauthUriRequest,
111111
): Result<GenericUri> = runCatchingUnlessCancelled {
112-
sessionManager.startOauthSessionUri(request)
112+
when (val pendingToken = sessionManager.initiateOauthSession(request)) {
113+
is SavedState.AuthTokens.Pending.DPoP -> {
114+
savedStateDataSource.setAuth(pendingToken)
115+
pendingToken.authorizeRequestUrl
116+
.let(::GenericUri)
117+
}
118+
}
113119
}
114120

115121
override suspend fun createSession(

data/core/src/commonMain/kotlin/com/tunjid/heron/data/repository/SavedStateDataSource.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.tunjid.heron.data.core.models.ContentLabelPreference
2424
import com.tunjid.heron.data.core.models.Label
2525
import com.tunjid.heron.data.core.models.Preferences
2626
import com.tunjid.heron.data.core.models.Server
27+
import com.tunjid.heron.data.core.types.ProfileHandle
2728
import com.tunjid.heron.data.core.types.ProfileId
2829
import com.tunjid.heron.data.core.types.Uri
2930
import com.tunjid.heron.data.core.utilities.Outcome
@@ -73,6 +74,23 @@ abstract class SavedState {
7374
override val authProfileId: ProfileId = Constants.unknownAuthorId
7475
}
7576

77+
@Serializable
78+
sealed class Pending : AuthTokens() {
79+
80+
@Serializable
81+
data class DPoP(
82+
val profileHandle: ProfileHandle,
83+
val endpoint: String,
84+
val authorizeRequestUrl: String,
85+
val codeVerifier: String,
86+
val nonce: String,
87+
val state: String,
88+
val expiresAt: Instant,
89+
) : Pending() {
90+
override val authProfileId: ProfileId = Constants.unknownAuthorId
91+
}
92+
}
93+
7694
@Serializable
7795
sealed class Authenticated : AuthTokens() {
7896

@@ -194,13 +212,15 @@ internal fun SavedState.signedProfilePreferencesOrDefault(): Preferences =
194212
is SavedState.AuthTokens.Authenticated.Bearer -> authTokens.authEndpoint
195213
is SavedState.AuthTokens.Authenticated.DPoP -> authTokens.issuerEndpoint
196214
is SavedState.AuthTokens.Guest -> authTokens.server.endpoint
215+
is SavedState.AuthTokens.Pending.DPoP -> authTokens.endpoint
197216
null -> Server.BlueSky.endpoint
198217
}.let(::preferencesForUrl)
199218

200219
private fun SavedState.AuthTokens?.ifSignedIn(): SavedState.AuthTokens.Authenticated? =
201220
when (this) {
202221
is SavedState.AuthTokens.Authenticated -> this
203222
is SavedState.AuthTokens.Guest,
223+
is SavedState.AuthTokens.Pending,
204224
null,
205225
-> null
206226
}
@@ -378,6 +398,6 @@ internal inline fun <T> SavedStateDataSource.singleSessionFlow(
378398
block(signedInProfileId)
379399
}
380400

381-
internal fun expiredSessionOutcome() = Outcome.Failure(ExpiredSessionException)
401+
internal fun expiredSessionOutcome() = Outcome.Failure(ExpiredSessionException())
382402

383-
private object ExpiredSessionException : IOException()
403+
private class ExpiredSessionException : IOException()

0 commit comments

Comments
 (0)