|
17 | 17 | package org.radarbase.android.auth.oauth2 |
18 | 18 |
|
19 | 19 | import android.app.Activity |
20 | | -import org.json.JSONException |
21 | | -import org.radarbase.android.RadarApplication.Companion.radarApp |
| 20 | +import android.content.Intent |
| 21 | +import androidx.activity.result.ActivityResultLauncher |
| 22 | +import androidx.lifecycle.Observer |
| 23 | +import org.radarbase.android.RadarApplication.Companion.radarConfig |
| 24 | +import org.radarbase.android.RadarConfiguration.Companion.OAUTH2_CLIENT_ID |
| 25 | +import org.radarbase.android.RadarConfiguration.Companion.OAUTH2_CLIENT_SECRET |
| 26 | +import org.radarbase.android.RadarConfiguration.Companion.SEP_URL_KEY |
| 27 | +import org.radarbase.android.RadarConfiguration.Companion.UNSAFE_KAFKA_CONNECTION |
22 | 28 | import org.radarbase.android.auth.* |
23 | | -import org.radarbase.android.auth.portal.ManagementPortalLoginManager |
24 | | -import org.radarbase.producer.AuthenticationException |
| 29 | +import org.radarbase.android.auth.commons.AbstractRadarLoginManager |
| 30 | +import org.radarbase.android.auth.commons.AbstractRadarPortalClient |
| 31 | +import org.radarbase.android.auth.commons.AuthType |
| 32 | +import org.radarbase.android.auth.portal.GetSubjectParser.Companion.SOURCE_TYPE_OAUTH2 |
| 33 | +import org.radarbase.android.auth.sep.SEPLoginManager.SEPClientConfig |
| 34 | +import org.radarbase.android.config.SingleRadarConfiguration |
| 35 | +import org.radarbase.producer.rest.RestClient |
| 36 | +import org.slf4j.LoggerFactory |
| 37 | +import java.net.MalformedURLException |
25 | 38 |
|
26 | 39 | /** |
27 | | - * Authenticates against the RADAR Management Portal. |
| 40 | + * Handles OAuth2 login flows using the [AppAuth-Android](https://openid.github.io/AppAuth-Android/) library. |
| 41 | + * |
| 42 | + * Provides initialization, token refresh, interactive login |
| 43 | + * with activity launchers, and response handling for OAuth2 |
| 44 | + * providers configured in RADAR. |
| 45 | + * |
| 46 | + * @property service the [AuthService] for callback dispatching. |
| 47 | + * |
| 48 | + * @see OAuth2StateManager |
| 49 | + * @see AbstractRadarLoginManager |
28 | 50 | */ |
29 | 51 | class OAuth2LoginManager( |
30 | 52 | private val service: AuthService, |
31 | | - private val projectIdClaim: String, |
32 | | - private val userIdClaim: String |
33 | | -) : LoginManager, LoginListener { |
34 | | - private val stateManager: OAuth2StateManager = OAuth2StateManager(service) |
| 53 | + authState: AppAuthState |
| 54 | +) : AbstractRadarLoginManager(service, AuthType.OAUTH2), LoginListener { |
| 55 | + |
| 56 | + private val config = service.radarConfig |
| 57 | + private var stateManager = OAuth2StateManager(config, this, service) |
| 58 | + |
| 59 | + override var client: AbstractRadarPortalClient? = null |
| 60 | + private var clientConfig: SEPClientConfig? = null |
| 61 | + private var restClient: RestClient? = null |
| 62 | + private var configUpdateObserver: Observer<SingleRadarConfiguration> = Observer { |
| 63 | + ensureOAuthClientConnectivity(it) |
| 64 | + } |
| 65 | + |
| 66 | + init { |
| 67 | + mainHandler.post { |
| 68 | + config.config.observeForever(configUpdateObserver) |
| 69 | + } |
| 70 | + updateSources(authState) |
| 71 | + } |
35 | 72 |
|
36 | 73 | override fun refresh(authState: AppAuthState): Boolean { |
37 | | - if (authState.tokenType != LoginManager.AUTH_TYPE_BEARER) { |
| 74 | + if (authState.tokenType != LoginManager.AUTH_TYPE_BEARER || authState.getAttribute( |
| 75 | + OAUTH2_REFRESH_TOKEN_PROPERTY |
| 76 | + ) == null |
| 77 | + ) { |
38 | 78 | return false |
39 | 79 | } |
40 | | - return authState.getAttribute(LOGIN_REFRESH_TOKEN) |
41 | | - ?.also { stateManager.refresh(service, it) } != null |
| 80 | + ensureOAuthClientConnectivity(config.latestConfig) |
| 81 | + return authState.getAttribute(OAUTH2_REFRESH_TOKEN_PROPERTY) |
| 82 | + ?.also { stateManager.refresh(service, authState, it, client) } != null |
42 | 83 | } |
43 | 84 |
|
| 85 | + override fun isRefreshable(authState: AppAuthState): Boolean { |
| 86 | + return authState.userId != null |
| 87 | + && authState.projectId != null |
| 88 | + && authState.getAttribute(OAUTH2_REFRESH_TOKEN_PROPERTY) != null |
| 89 | + } |
44 | 90 |
|
45 | | - override fun isRefreshable(authState: AppAuthState): Boolean = |
46 | | - authState.userId != null && authState.getAttribute(LOGIN_REFRESH_TOKEN) != null |
47 | | - |
48 | | - override fun start(authState: AppAuthState) { |
49 | | - service.radarApp.let { app -> |
50 | | - stateManager.login(service, app.loginActivity, app.configuration.latestConfig) |
| 91 | + override fun start( |
| 92 | + authState: AppAuthState, |
| 93 | + activityResultLauncher: ActivityResultLauncher<Intent>? |
| 94 | + ) { |
| 95 | + requireNotNull(activityResultLauncher) { |
| 96 | + "Activity result launcher can't be null in OAuthLoginManager" |
51 | 97 | } |
| 98 | + val latestConfig = config.latestConfig |
| 99 | + |
| 100 | + ensureOAuthClientConnectivity(latestConfig) |
| 101 | + stateManager.login(latestConfig, activityResultLauncher) |
52 | 102 | } |
53 | 103 |
|
54 | | - override fun onActivityCreate(activity: Activity): Boolean { |
55 | | - stateManager.updateAfterAuthorization(service, activity.intent) |
| 104 | + override fun onActivityCreate( |
| 105 | + activity: Activity, |
| 106 | + binder: AuthService.AuthServiceBinder |
| 107 | + ): Boolean { |
| 108 | + stateManager.updateAfterAuthorization(service, activity.intent, binder, client) |
56 | 109 | return true |
57 | 110 | } |
58 | 111 |
|
59 | | - override fun invalidate(authState: AppAuthState, disableRefresh: Boolean): AppAuthState? = |
60 | | - authState.takeIf { it.authenticationSource == OAUTH2_SOURCE_TYPE } |
| 112 | + override fun invalidate(authState: AppAuthState, disableRefresh: Boolean): AppAuthState? { |
| 113 | + return when { |
| 114 | + authState.authenticationSource != SOURCE_TYPE_OAUTH2 -> null |
| 115 | + disableRefresh -> authState.alter { |
| 116 | + attributes -= OAUTH2_REFRESH_TOKEN_PROPERTY |
| 117 | + isPrivacyPolicyAccepted = false |
| 118 | + } |
| 119 | + |
| 120 | + else -> authState |
| 121 | + } |
| 122 | + } |
61 | 123 |
|
62 | 124 | override val sourceTypes: List<String> = OAUTH2_SOURCE_TYPES |
63 | 125 |
|
64 | | - @Throws(AuthenticationException::class) |
65 | | - override fun registerSource(authState: AppAuthState, source: SourceMetadata, |
66 | | - success: (AppAuthState, SourceMetadata) -> Unit, |
67 | | - failure: (Exception?) -> Unit): Boolean { |
68 | | - success(authState, source) |
69 | | - return true |
70 | | - } |
| 126 | + @Synchronized |
| 127 | + private fun ensureOAuthClientConnectivity(config: SingleRadarConfiguration) { |
| 128 | + val oauthClientConfig = try { |
| 129 | + SEPClientConfig( |
| 130 | + config.getString(SEP_URL_KEY), |
| 131 | + config.getBoolean(UNSAFE_KAFKA_CONNECTION, false), |
| 132 | + config.getString(OAUTH2_CLIENT_ID), |
| 133 | + config.getString(OAUTH2_CLIENT_SECRET, ""), |
| 134 | + ) |
| 135 | + } catch (_: MalformedURLException) { |
| 136 | + logger.error("Cannot construct OAuth client with malformed URL") |
| 137 | + null |
| 138 | + } catch (_: IllegalArgumentException) { |
| 139 | + logger.error("Cannot construct OAuth client without client credentials") |
| 140 | + null |
| 141 | + } |
71 | 142 |
|
72 | | - @Throws(AuthenticationException::class) |
73 | | - override fun updateSource(appAuth: AppAuthState, source: SourceMetadata, |
74 | | - success: (AppAuthState, SourceMetadata) -> Unit, |
75 | | - failure: (Exception?) -> Unit): Boolean { |
76 | | - success(appAuth, source) |
77 | | - return true |
| 143 | + if (oauthClientConfig == clientConfig) return |
| 144 | + |
| 145 | + client = oauthClientConfig?.let { oauthConfig -> |
| 146 | + OAuth2Client( |
| 147 | + oauthConfig.serverConfig, |
| 148 | + oauthConfig.clientId, |
| 149 | + oauthConfig.clientSecret, |
| 150 | + client = restClient |
| 151 | + ).also { oauth -> |
| 152 | + restClient = oauth.client |
| 153 | + clientConfig = oauthConfig |
| 154 | + } |
| 155 | + } |
78 | 156 | } |
79 | 157 |
|
80 | | - override fun onDestroy() = Unit |
81 | | - |
82 | 158 | override fun loginSucceeded(manager: LoginManager?, authState: AppAuthState) { |
83 | 159 | val token = authState.token |
84 | 160 | if (token == null) { |
85 | | - loginFailed(this, |
86 | | - IllegalArgumentException("Cannot login using OAuth2 without a token")) |
| 161 | + loginFailed( |
| 162 | + this, |
| 163 | + IllegalArgumentException("Cannot login using OAuth2 without a token") |
| 164 | + ) |
87 | 165 | return |
88 | 166 | } |
89 | | - try { |
90 | | - processJwt(authState, Jwt.parse(token)).let { |
91 | | - service.loginSucceeded(this, it) |
92 | | - } |
93 | | - } catch (ex: JSONException) { |
94 | | - loginFailed(this, ex) |
95 | | - } |
96 | | - |
| 167 | + logger.info("Updating sources with latest state") |
| 168 | + updateSources(authState) |
| 169 | + service.loginSucceeded(manager, authState) |
97 | 170 | } |
98 | 171 |
|
99 | | - private fun processJwt(authState: AppAuthState, jwt: Jwt): AppAuthState { |
100 | | - val body = jwt.body |
| 172 | + override fun loginFailed(manager: LoginManager?, ex: Exception?) = |
| 173 | + this.service.loginFailed(this, ex) |
101 | 174 |
|
102 | | - return authState.alter { |
103 | | - authenticationSource = OAUTH2_SOURCE_TYPE |
104 | | - needsRegisteredSources = false |
105 | | - projectId = body.optString(projectIdClaim) |
106 | | - userId = body.optString(userIdClaim) |
107 | | - expiration = body.optLong("exp", java.lang.Long.MAX_VALUE / 1000L) * 1000L |
108 | | - } |
| 175 | + override fun onDestroy() { |
| 176 | + config.config.removeObserver(configUpdateObserver) |
| 177 | + stateManager.stop() |
109 | 178 | } |
110 | 179 |
|
111 | | - override fun loginFailed(manager: LoginManager?, ex: Exception?) = this.service.loginFailed(this, ex) |
112 | | - |
113 | 180 | companion object { |
114 | | - private const val OAUTH2_SOURCE_TYPE = "org.radarcns.android.auth.oauth2.OAuth2LoginManager" |
115 | | - private val OAUTH2_SOURCE_TYPES = listOf(OAUTH2_SOURCE_TYPE) |
116 | | - const val LOGIN_REFRESH_TOKEN = "org.radarcns.auth.OAuth2LoginManager.refreshToken" |
| 181 | + private val logger = LoggerFactory.getLogger(OAuth2LoginManager::class.java) |
| 182 | + |
| 183 | + const val OAUTH2_REFRESH_TOKEN_PROPERTY = |
| 184 | + "org.radarbase.auth.OAuth2LoginManager.refreshToken" |
| 185 | + val OAUTH2_SOURCE_TYPES = listOf(SOURCE_TYPE_OAUTH2) |
117 | 186 | } |
118 | 187 | } |
0 commit comments