Skip to content

Commit 7da78f4

Browse files
Merge pull request #486 from RADAR-base/sep-login
Implementation of Self Enrollment Portal (SEP) QR Code and OAuth2 Login Flows
2 parents e2dacb8 + 59987d4 commit 7da78f4

27 files changed

+1374
-566
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ android.defaults.buildfeatures.buildconfig=true
2424
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
2525
# org.gradle.parallel=true
2626

27-
project_version=1.4.1
27+
project_version=1.4.2-SNAPSHOT
2828

2929
java_version=17
3030
kotlin_version=1.9.23

plugins/radar-android-login-oauth2/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apply from: "$rootDir/gradle/android.gradle"
22

33
android {
44
namespace "org.radarbase.android.auth.oauth2"
5-
defaultConfig.manifestPlaceholders = ["appAuthRedirectScheme": "org.radarbase.android"]
5+
defaultConfig.manifestPlaceholders = ["appAuthRedirectScheme": "org.radarbase.prmt"]
66
}
77

88
description = "RADAR Android OAuth2 LoginManager."
Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,19 @@
1-
<manifest/>
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools">
3+
<application>
4+
<activity
5+
android:name="net.openid.appauth.RedirectUriReceiverActivity"
6+
android:exported="true"
7+
tools:node="replace">
8+
<intent-filter>
9+
<action android:name="android.intent.action.VIEW"/>
10+
<category android:name="android.intent.category.DEFAULT"/>
11+
<category android:name="android.intent.category.BROWSABLE"/>
12+
<data
13+
android:scheme="org.radarbase.prmt"
14+
android:host="login"
15+
/>
16+
</intent-filter>
17+
</activity>
18+
</application>
19+
</manifest>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.radarbase.android.auth.oauth2
2+
3+
import org.radarbase.android.auth.sep.SEPClient
4+
import org.radarbase.config.ServerConfig
5+
import org.radarbase.producer.rest.RestClient
6+
7+
class OAuth2Client(
8+
serverConfig: ServerConfig,
9+
clientId: String,
10+
clientSecret: String,
11+
client: RestClient?
12+
) : SEPClient(serverConfig, clientId, clientSecret, client)

plugins/radar-android-login-oauth2/src/main/java/org/radarbase/android/auth/oauth2/OAuth2LoginManager.kt

Lines changed: 130 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -17,102 +17,171 @@
1717
package org.radarbase.android.auth.oauth2
1818

1919
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
2228
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
2538

2639
/**
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
2850
*/
2951
class OAuth2LoginManager(
3052
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+
}
3572

3673
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+
) {
3878
return false
3979
}
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
4283
}
4384

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+
}
4490

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"
5197
}
98+
val latestConfig = config.latestConfig
99+
100+
ensureOAuthClientConnectivity(latestConfig)
101+
stateManager.login(latestConfig, activityResultLauncher)
52102
}
53103

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)
56109
return true
57110
}
58111

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+
}
61123

62124
override val sourceTypes: List<String> = OAUTH2_SOURCE_TYPES
63125

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+
}
71142

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+
}
78156
}
79157

80-
override fun onDestroy() = Unit
81-
82158
override fun loginSucceeded(manager: LoginManager?, authState: AppAuthState) {
83159
val token = authState.token
84160
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+
)
87165
return
88166
}
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)
97170
}
98171

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)
101174

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()
109178
}
110179

111-
override fun loginFailed(manager: LoginManager?, ex: Exception?) = this.service.loginFailed(this, ex)
112-
113180
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)
117186
}
118187
}

0 commit comments

Comments
 (0)