55
66package org.microg.gms.auth.credentials.identity
77
8+ import android.accounts.Account
89import android.accounts.AccountManager
910import android.app.PendingIntent
1011import android.app.PendingIntent.FLAG_IMMUTABLE
@@ -27,6 +28,7 @@ import com.google.android.gms.auth.api.identity.internal.IVerifyWithGoogleCallba
2728import com.google.android.gms.auth.api.signin.GoogleSignInOptions
2829import com.google.android.gms.auth.api.signin.internal.SignInConfiguration
2930import com.google.android.gms.common.ConnectionResult
31+ import com.google.android.gms.common.api.CommonStatusCodes
3032import com.google.android.gms.common.api.Scope
3133import com.google.android.gms.common.api.Status
3234import com.google.android.gms.common.api.internal.IStatusCallback
@@ -35,11 +37,15 @@ import com.google.android.gms.common.internal.GetServiceRequest
3537import com.google.android.gms.common.internal.IGmsCallbacks
3638import kotlinx.coroutines.Dispatchers
3739import kotlinx.coroutines.withContext
40+ import okhttp3.FormBody
41+ import okhttp3.OkHttpClient
42+ import okhttp3.Request
3843import org.microg.gms.BaseService
3944import org.microg.gms.auth.AuthConstants
4045import org.microg.gms.auth.credentials.FEATURES
4146import org.microg.gms.auth.signin.AuthSignInActivity
4247import org.microg.gms.auth.signin.SignInConfigurationService
48+ import org.microg.gms.auth.signin.checkAccountAuthStatus
4349import org.microg.gms.auth.signin.getOAuthManager
4450import org.microg.gms.auth.signin.getServerAuthTokenManager
4551import org.microg.gms.auth.signin.performSignIn
@@ -48,18 +54,19 @@ import org.microg.gms.common.AccountUtils
4854import org.microg.gms.common.Constants
4955import org.microg.gms.common.GmsService
5056import org.microg.gms.common.PackageUtils
57+ import java.util.Locale
58+ import java.util.concurrent.TimeUnit
5159import java.util.concurrent.atomic.AtomicInteger
5260
5361private const val TAG = " AuthorizationService"
62+ private const val REVOKE_ENDPOINT = " https://oauth2.googleapis.com/revoke"
5463
5564class AuthorizationService : BaseService (TAG , GmsService .AUTH_API_IDENTITY_AUTHORIZATION ) {
5665
5766 override fun handleServiceRequest (callback : IGmsCallbacks , request : GetServiceRequest , service : GmsService ) {
5867 Log .d(TAG , " handleServiceRequest start " )
59- val packageName = PackageUtils .getAndCheckCallingPackage(this , request.packageName)
60- ? : throw IllegalArgumentException (" Missing package name" )
61- val connectionInfo = ConnectionInfo ()
62- connectionInfo.features = FEATURES
68+ val packageName = PackageUtils .getAndCheckCallingPackage(this , request.packageName) ? : throw IllegalArgumentException (" Missing package name" )
69+ val connectionInfo = ConnectionInfo ().apply { features = FEATURES }
6370 callback.onPostInitCompleteWithConnectionInfo(
6471 ConnectionResult .SUCCESS , AuthorizationServiceImpl (this , packageName, this .lifecycle).asBinder(), connectionInfo
6572 )
@@ -68,115 +75,201 @@ class AuthorizationService : BaseService(TAG, GmsService.AUTH_API_IDENTITY_AUTHO
6875
6976class AuthorizationServiceImpl (val context : Context , val packageName : String , override val lifecycle : Lifecycle ) : IAuthorizationService.Stub(), LifecycleOwner {
7077
71- companion object {
78+ companion object {
7279 private val nextRequestCode = AtomicInteger (0 )
80+ private val httpClient: OkHttpClient by lazy {
81+ OkHttpClient .Builder ().connectTimeout(10 , TimeUnit .SECONDS ).readTimeout(10 , TimeUnit .SECONDS ).build()
82+ }
7383 }
7484
7585 override fun authorize (callback : IAuthorizationCallback ? , request : AuthorizationRequest ? ) {
76- Log .d(TAG , " Method: authorize called, packageName: $packageName request: $request " )
86+ Log .d(TAG , " authorize called, packageName= $packageName request= $request " )
7787 lifecycleScope.launchWhenStarted {
78- val requestAccount = request?.account
79- val account = requestAccount ? : AccountUtils .get(context).getSelectedAccount(packageName)
80- val googleSignInOptions = GoogleSignInOptions .Builder ().apply {
81- request?.requestedScopes?.forEach { requestScopes(it) }
82- if (request?.idTokenRequested == true && request.serverClientId != null ) {
83- if (account?.name != requestAccount?.name) {
84- requestEmail().requestProfile()
85- }
86- requestIdToken(request.serverClientId)
87- }
88- if (request?.serverAuthCodeRequested == true && request.serverClientId != null ) requestServerAuthCode(request.serverClientId, request.forceCodeForRefreshToken)
89- }.build()
90- Log .d(TAG , " authorize: account: ${account?.name} " )
91- val result = if (account != null ) {
92- val (accessToken, signInAccount) = performSignIn(context, packageName, googleSignInOptions, account, false )
93- if (requestAccount != null ) {
94- AccountUtils .get(context).saveSelectedAccount(packageName, requestAccount)
95- }
96- AuthorizationResult (
97- signInAccount?.serverAuthCode,
98- accessToken,
99- signInAccount?.idToken,
100- signInAccount?.grantedScopes?.toList().orEmpty().map { it.scopeUri },
101- signInAccount,
102- null
103- )
104- } else {
105- val options = GoogleSignInOptions .Builder (googleSignInOptions).apply {
106- val defaultAccount = SignInConfigurationService .getDefaultAccount(context, packageName)
107- defaultAccount?.name?.let { setAccountName(it) }
108- }.build()
109- val intent = Intent (context, AuthSignInActivity ::class .java).apply {
110- `package` = Constants .GMS_PACKAGE_NAME
111- putExtra(" config" , SignInConfiguration (packageName, options))
112- }
113- AuthorizationResult (
114- null ,
115- null ,
116- null ,
117- request?.requestedScopes.orEmpty().map { it.scopeUri },
118- null ,
119- PendingIntent .getActivity(context, nextRequestCode.incrementAndGet(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE )
120- )
88+ try {
89+ val result = performAuthorize(request)
90+ Log .d(TAG , " authorize resolved: ${if (result.pendingIntent != null ) " pendingIntent" else " silent" } , grantedScopes=${result.grantedScopes.size} " )
91+ runCatching { callback?.onAuthorized(Status .SUCCESS , result) }
92+ } catch (e: InvalidAccountException ) {
93+ Log .w(TAG , " authorize: invalid account" , e)
94+ runCatching { callback?.onAuthorized(Status (CommonStatusCodes .INVALID_ACCOUNT ), null ) }
95+ } catch (e: Exception ) {
96+ Log .w(TAG , " authorize failed, falling back to PendingIntent" , e)
97+ runCatching { callback?.onAuthorized(Status .SUCCESS , buildPendingIntentResult(request)) }
12198 }
122- runCatching {
123- callback?.onAuthorized(Status .SUCCESS , result.also { Log .d(TAG , " authorize: result:$it " ) })
99+ }
100+ }
101+
102+ private suspend fun performAuthorize (request : AuthorizationRequest ? ): AuthorizationResult {
103+ require(request?.requestedScopes?.isNotEmpty() == true ) { " requestedScopes cannot be null or empty" }
104+
105+ val requestAccount = request!! .account
106+ val candidate = requestAccount ? : AccountUtils .get(context).getSelectedAccount(packageName) ? : SignInConfigurationService .getDefaultAccount(context, packageName)
107+ if (candidate == null || request.forceCodeForRefreshToken) {
108+ return buildPendingIntentResult(request)
109+ }
110+
111+ val account = AccountManager .get(context).getAccountsByType(AuthConstants .DEFAULT_ACCOUNT_TYPE ).firstOrNull { it == candidate } ? : run {
112+ AccountUtils .get(context).removeSelectedAccount(packageName)
113+ return buildPendingIntentResult(request)
114+ }
115+
116+ val hostedDomain = request.hostedDomainFilter
117+ if (! hostedDomain.isNullOrEmpty() && ! account.name.lowercase(Locale .ROOT ).endsWith(" @${hostedDomain.lowercase(Locale .ROOT )} " )) {
118+ throw InvalidAccountException (" account ${account.name} does not match hostedDomainFilter=$hostedDomain " )
119+ }
120+
121+ val crossAccount = requestAccount != null && account.name != requestAccount.name
122+ val options = buildSignInOptions(request, crossAccount)
123+ val includeGrantedScopes = if (request.offlineAccess) " 0" else " 1"
124+ val (accessToken, signInAccount) = performSignIn(context, packageName, options, account, false , includeGrantedScopes = includeGrantedScopes)
125+ if (accessToken == null || signInAccount == null ) {
126+ return buildPendingIntentResult(request)
127+ }
128+
129+ if (requestAccount != null ) {
130+ AccountUtils .get(context).saveSelectedAccount(packageName, requestAccount)
131+ }
132+
133+ return AuthorizationResult (
134+ signInAccount.serverAuthCode,
135+ accessToken,
136+ signInAccount.idToken,
137+ signInAccount.grantedScopes.toList().map { it.scopeUri },
138+ signInAccount,
139+ null ,
140+ )
141+ }
142+
143+ private fun buildSignInOptions (request : AuthorizationRequest , crossAccount : Boolean ): GoogleSignInOptions {
144+ return GoogleSignInOptions .Builder ().apply {
145+ request.requestedScopes?.forEach { requestScopes(it) }
146+ val clientId = request.serverClientId
147+ if (request.idTokenRequested && clientId != null ) {
148+ if (crossAccount) requestEmail().requestProfile()
149+ requestIdToken(clientId)
150+ }
151+ if (request.serverAuthCodeRequested && clientId != null ) {
152+ requestServerAuthCode(clientId, request.forceCodeForRefreshToken)
153+ }
154+ }.build()
155+ }
156+
157+ private suspend fun buildPendingIntentResult (request : AuthorizationRequest ? ): AuthorizationResult {
158+ val defaultAccountName = SignInConfigurationService .getDefaultAccount(context, packageName)?.name
159+ val options = GoogleSignInOptions .Builder ().apply {
160+ request?.requestedScopes?.forEach { requestScopes(it) }
161+ val clientId = request?.serverClientId
162+ if (request?.idTokenRequested == true && clientId != null ) {
163+ requestEmail().requestProfile().requestIdToken(clientId)
124164 }
165+ if (request?.serverAuthCodeRequested == true && clientId != null ) {
166+ requestServerAuthCode(clientId, request.forceCodeForRefreshToken)
167+ }
168+ defaultAccountName?.let { setAccountName(it) }
169+ }.build()
170+ val intent = Intent (context, AuthSignInActivity ::class .java).apply {
171+ `package` = Constants .GMS_PACKAGE_NAME
172+ putExtra(" config" , SignInConfiguration (packageName, options))
125173 }
174+ val pendingIntent = PendingIntent .getActivity(
175+ context,
176+ nextRequestCode.incrementAndGet(),
177+ intent,
178+ FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE ,
179+ )
180+ return AuthorizationResult (
181+ null , null , null ,
182+ request?.requestedScopes.orEmpty().map { it.scopeUri },
183+ null ,
184+ pendingIntent,
185+ )
126186 }
127187
128188 override fun verifyWithGoogle (callback : IVerifyWithGoogleCallback ? , request : VerifyWithGoogleRequest ? ) {
129- Log .d(TAG , " unimplemented Method: verifyWithGoogle: request: $request " )
189+ Log .d(TAG , " verifyWithGoogle called, request= $request " )
130190 lifecycleScope.launchWhenStarted {
131- val account = AccountUtils .get(context).getSelectedAccount(packageName) ? : SignInConfigurationService .getDefaultAccount(context, packageName)
132- if (account == null ) {
133- Log .d(TAG , " Method: authorize called, but account is null" )
134- callback?.onVerifed(Status .CANCELED , null )
135- return @launchWhenStarted
191+ val result = runCatching { performVerify(request) }.onFailure { Log .w(TAG , " verifyWithGoogle failed" , it) }.getOrNull()
192+ val status = if (result != null ) Status .SUCCESS else Status .CANCELED
193+ runCatching { callback?.onVerifed(status, result) }
194+ }
195+ }
196+
197+ private suspend fun performVerify (request : VerifyWithGoogleRequest ? ): VerifyWithGoogleResult ? {
198+ val req = request?.takeIf { it.requestedScopes?.isNotEmpty() == true } ? : return null
199+ val account = AccountUtils .get(context).getSelectedAccount(packageName) ? : SignInConfigurationService .getDefaultAccount(context, packageName) ? : return null
200+
201+ val options = GoogleSignInOptions .Builder ().apply {
202+ req.requestedScopes?.forEach { requestScopes(it) }
203+ if (req.offlineAccess && req.serverClientId != null ) {
204+ requestServerAuthCode(req.serverClientId)
136205 }
137- if (request?.offlineAccess == true && request.serverClientId != null ) {
138- val googleSignInOptions = GoogleSignInOptions .Builder ().apply {
139- request.requestedScopes?.forEach { requestScopes(it) }
140- requestServerAuthCode(request.serverClientId)
141- }.build()
142- val authResponse = getServerAuthTokenManager(context, packageName, googleSignInOptions, account)?.let {
143- withContext(Dispatchers .IO ) { it.requestAuth(true ) }
144- }
145- callback?.onVerifed(Status .SUCCESS , VerifyWithGoogleResult ().apply {
146- serverAuthToken = authResponse?.auth
147- grantedScopes = authResponse?.grantedScopes?.split(" " )?.map { Scope (it) }?.toList() ? : googleSignInOptions.scopeUris.toList()
148- })
149- return @launchWhenStarted
206+ }.build()
207+
208+ if (req.offlineAccess && req.serverClientId != null ) {
209+ val authResponse = getServerAuthTokenManager(context, packageName, options, account)?.let {
210+ withContext(Dispatchers .IO ) { it.requestAuth(true ) }
211+ } ? : return null
212+ if (authResponse.auth == null ) return null
213+ return VerifyWithGoogleResult ().apply {
214+ serverAuthToken = authResponse.auth
215+ grantedScopes = authResponse.grantedScopes?.split(" " )?.map { Scope (it) } ? : options.scopeUris.toList()
150216 }
151- callback?.onVerifed(Status .CANCELED , null )
152217 }
218+
219+ val granted = checkAccountAuthStatus(context, packageName, options.scopes.toList(), account)
220+ if (! granted) return null
221+ return VerifyWithGoogleResult ().apply { grantedScopes = options.scopeUris.toList() }
153222 }
154223
155224 override fun revokeAccess (callback : IStatusCallback ? , request : RevokeAccessRequest ? ) {
156- Log .d(TAG , " Method: revokeAccess called, request: $request " )
225+ Log .d(TAG , " revokeAccess called, request= $request " )
157226 lifecycleScope.launchWhenStarted {
227+ runCatching { performRevoke(request) }.onFailure { Log .w(TAG , " revokeAccess failed" , it) }
228+ runCatching { callback?.onResult(Status .SUCCESS ) }
229+ }
230+ }
231+
232+ private suspend fun performRevoke (request : RevokeAccessRequest ? ) {
233+ val account: Account ? = request?.account
234+ ? : AccountUtils .get(context).getSelectedAccount(packageName)
235+ ? : SignInConfigurationService .getDefaultAccount(context, packageName)
236+
237+ if (account != null ) {
158238 val authOptions = SignInConfigurationService .getAuthOptions(context, packageName)
159- val authAccount = request?.account
160- if (authOptions.isNotEmpty() && authAccount != null ) {
161- val authManager = getOAuthManager(context, packageName, authOptions.first(), authAccount)
162- val token = authManager.peekAuthToken()
163- if (token != null ) {
164- // todo "https://oauth2.googleapis.com/revoke"
165- authManager.invalidateAuthToken(token)
166- authManager.isPermitted = false
167- }
239+ for (options in authOptions) {
240+ val authManager = getOAuthManager(context, packageName, options, account)
241+ val token = authManager.peekAuthToken() ? : continue
242+ runCatching { revokeTokenRemotely(token) }.onFailure { Log .w(TAG , " remote revoke failed (continuing local invalidate)" , it) }
243+ authManager.invalidateAuthToken(token)
244+ authManager.isPermitted = false
245+ }
246+ }
247+
248+ AccountUtils .get(context).removeSelectedAccount(packageName)
249+ SignInConfigurationService .setAuthInfo(context, packageName, null , null )
250+ }
251+
252+ private suspend fun revokeTokenRemotely (token : String ) {
253+ withContext(Dispatchers .IO ) {
254+ val body = FormBody .Builder ().add(" token" , token).build()
255+ val request = Request .Builder ().url(REVOKE_ENDPOINT ).post(body).build()
256+ httpClient.newCall(request).execute().use { response ->
257+ Log .d(TAG , " revoke endpoint status=${response.code} " )
168258 }
169- AccountUtils .get(context).removeSelectedAccount(packageName)
170- runCatching { callback?.onResult(Status .SUCCESS ) }
171259 }
172260 }
173261
174262 override fun clearToken (callback : IStatusCallback ? , request : ClearTokenRequest ? ) {
175- Log .d(TAG , " Method: clearToken called, request:$request " )
176- request?.token?.let {
177- AccountManager .get(context).invalidateAuthToken(AuthConstants .DEFAULT_ACCOUNT_TYPE , it)
263+ Log .d(TAG , " clearToken called, request=$request " )
264+ lifecycleScope.launchWhenStarted {
265+ runCatching {
266+ request?.token?.takeIf { it.isNotEmpty() }?.let {
267+ AccountManager .get(context).invalidateAuthToken(AuthConstants .DEFAULT_ACCOUNT_TYPE , it)
268+ }
269+ }.onFailure { Log .w(TAG , " clearToken failed" , it) }
270+ runCatching { callback?.onResult(Status .SUCCESS ) }
178271 }
179- runCatching { callback?.onResult(Status .SUCCESS ) }
180272 }
181273
274+ private class InvalidAccountException (message : String ) : Exception(message)
182275}
0 commit comments