@@ -22,14 +22,11 @@ import com.atproto.server.RefreshSessionResponse
2222import com.tunjid.heron.data.core.models.OauthUriRequest
2323import com.tunjid.heron.data.core.models.Server
2424import 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
2725import com.tunjid.heron.data.core.types.ProfileId
2826import com.tunjid.heron.data.lexicons.XrpcBlueskyApi
2927import com.tunjid.heron.data.lexicons.XrpcSerializersModule
3028import com.tunjid.heron.data.network.oauth.DpopKeyPair
3129import com.tunjid.heron.data.network.oauth.OAuthApi
32- import com.tunjid.heron.data.network.oauth.OAuthAuthorizationRequest
3330import com.tunjid.heron.data.network.oauth.OAuthClient
3431import com.tunjid.heron.data.network.oauth.OAuthScope
3532import com.tunjid.heron.data.network.oauth.OAuthToken
@@ -65,6 +62,7 @@ import io.ktor.http.encodedPath
6562import io.ktor.http.isSuccess
6663import io.ktor.http.set
6764import io.ktor.http.takeFrom
65+ import kotlin.time.Clock
6866import kotlin.time.Duration.Companion.seconds
6967import kotlinx.coroutines.flow.MutableStateFlow
7068import kotlinx.coroutines.flow.first
@@ -77,9 +75,9 @@ import sh.christian.ozone.api.runtime.buildXrpcJsonConfiguration
7775
7876internal 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-
519523internal val BlueskyJson : Json = Json (
520524 from = buildXrpcJsonConfiguration(XrpcSerializersModule ),
521525 builderAction = {
0 commit comments