Skip to content

Commit 4425514

Browse files
authored
Auth: Improve AuthorizationService (#3421)
1 parent a46b32f commit 4425514

6 files changed

Lines changed: 209 additions & 99 deletions

File tree

play-services-base/core/src/main/java/org/microg/gms/auth/AuthRequest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public class AuthRequest extends HttpFormClient.Request {
9494
public String oauth2IncludeProfile;
9595
@RequestContent("oauth2_include_email")
9696
public String oauth2IncludeEmail;
97+
@RequestContent("include_granted_scopes")
98+
public String includeGrantedScopes;
9799
@HttpFormClient.RequestContentDynamic
98100
public Map<Object, Object> dynamicFields;
99101

@@ -238,6 +240,11 @@ public AuthRequest oauth2IncludeEmail(String oauth2IncludeEmail) {
238240
return this;
239241
}
240242

243+
public AuthRequest includeGrantedScopes(String includeGrantedScopes) {
244+
this.includeGrantedScopes = includeGrantedScopes;
245+
return this;
246+
}
247+
241248
public AuthRequest oauth2Prompt(String oauth2Prompt) {
242249
this.oauth2Prompt = oauth2Prompt;
243250
return this;

play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class AuthManager {
5454
private String tokenRequestOptions;
5555
public String includeEmail;
5656
public String includeProfile;
57+
public String includeGrantedScopes;
5758
public boolean isGmsApp;
5859
public boolean ignoreStoredPermission = false;
5960
public boolean forceRefreshToken = false;
@@ -339,6 +340,7 @@ public AuthResponse requestAuth(boolean legacy) throws IOException {
339340
.oauth2Prompt(oauth2Prompt)
340341
.oauth2IncludeProfile(includeProfile)
341342
.oauth2IncludeEmail(includeEmail)
343+
.includeGrantedScopes(includeGrantedScopes)
342344
.itCaveatTypes(itCaveatTypes)
343345
.tokenRequestOptions(tokenRequestOptions)
344346
.systemPartition(isSystemApp())

play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt

Lines changed: 180 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.microg.gms.auth.credentials.identity
77

8+
import android.accounts.Account
89
import android.accounts.AccountManager
910
import android.app.PendingIntent
1011
import android.app.PendingIntent.FLAG_IMMUTABLE
@@ -27,6 +28,7 @@ import com.google.android.gms.auth.api.identity.internal.IVerifyWithGoogleCallba
2728
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
2829
import com.google.android.gms.auth.api.signin.internal.SignInConfiguration
2930
import com.google.android.gms.common.ConnectionResult
31+
import com.google.android.gms.common.api.CommonStatusCodes
3032
import com.google.android.gms.common.api.Scope
3133
import com.google.android.gms.common.api.Status
3234
import com.google.android.gms.common.api.internal.IStatusCallback
@@ -35,11 +37,15 @@ import com.google.android.gms.common.internal.GetServiceRequest
3537
import com.google.android.gms.common.internal.IGmsCallbacks
3638
import kotlinx.coroutines.Dispatchers
3739
import kotlinx.coroutines.withContext
40+
import okhttp3.FormBody
41+
import okhttp3.OkHttpClient
42+
import okhttp3.Request
3843
import org.microg.gms.BaseService
3944
import org.microg.gms.auth.AuthConstants
4045
import org.microg.gms.auth.credentials.FEATURES
4146
import org.microg.gms.auth.signin.AuthSignInActivity
4247
import org.microg.gms.auth.signin.SignInConfigurationService
48+
import org.microg.gms.auth.signin.checkAccountAuthStatus
4349
import org.microg.gms.auth.signin.getOAuthManager
4450
import org.microg.gms.auth.signin.getServerAuthTokenManager
4551
import org.microg.gms.auth.signin.performSignIn
@@ -48,18 +54,19 @@ import org.microg.gms.common.AccountUtils
4854
import org.microg.gms.common.Constants
4955
import org.microg.gms.common.GmsService
5056
import org.microg.gms.common.PackageUtils
57+
import java.util.Locale
58+
import java.util.concurrent.TimeUnit
5159
import java.util.concurrent.atomic.AtomicInteger
5260

5361
private const val TAG = "AuthorizationService"
62+
private const val REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke"
5463

5564
class 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

6976
class 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
}

play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,9 @@ class IdentitySignInServiceImpl(private val context: Context, private val client
143143
}
144144
}
145145
AccountUtils.get(context).removeSelectedAccount(clientPackageName)
146+
SignInConfigurationService.setAuthInfo(context, clientPackageName, null, null)
147+
callback.onResult(Status.SUCCESS)
146148
}
147-
callback.onResult(Status.SUCCESS)
148149
}
149150

150151
override fun getSignInIntent(

0 commit comments

Comments
 (0)