Skip to content

Commit

Permalink
WIP: OAuth implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Iulia Stana committed Mar 1, 2023
1 parent 2ccede6 commit dd25e45
Show file tree
Hide file tree
Showing 12 changed files with 639 additions and 13 deletions.
6 changes: 4 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
kotlin("android")
kotlin("kapt")
id("kotlin-parcelize")
id("dagger.hilt.android.plugin")
id("com.google.dagger.hilt.android")
}

if (JavaVersion.current() < JavaVersion.VERSION_11) {
Expand Down Expand Up @@ -50,7 +50,7 @@ android {
manifestPlaceholders["tiqr_config_enroll_scheme"] = "eduidenroll"
manifestPlaceholders["tiqr_config_auth_scheme"] = "eduidauth"
manifestPlaceholders["tiqr_config_token_exchange_enabled"] = "false"

manifestPlaceholders["appAuthRedirectScheme"] = "login.test2.eduid"
// only package supported languages
resourceConfigurations += listOf("en", "nl")
vectorDrawables {
Expand Down Expand Up @@ -138,6 +138,7 @@ dependencies {
implementation(libs.androidx.compose.constraint)
implementation(libs.androidx.core)
implementation(libs.androidx.concurrent)
implementation(libs.androidx.datastore)
implementation(libs.androidx.lifecycle.common)
implementation(libs.androidx.lifecycle.livedata)
implementation(libs.androidx.localBroadcastManager)
Expand All @@ -148,6 +149,7 @@ dependencies {
implementation(libs.google.android.material)
implementation(libs.google.mlkit.barcode)
implementation(libs.google.firebase.messaging)
implementation(libs.appauth)

implementation(libs.dagger.hilt.android)
implementation(libs.dagger.hilt.fragment)
Expand Down
12 changes: 12 additions & 0 deletions app/src/debug/res/raw/auth_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"client_id": "dev.egeniq.nl",
"redirect_uri": "http://login.test2.eduid.nl/myconext/api/mobile/swagger-ui/oauth2-redirect.html",
"end_session_redirect_uri": "http://login.test2.eduid.nl",
"authorization_scope": "eduid.nl/mobile",
"discovery_uri": "https://connect.test2.surfconext.nl/oidc/.well-known/openid-configuration",
"authorization_endpoint_uri": "https://connect.test2.surfconext.nl/oidc/authorize",
"token_endpoint_uri": "https://connect.test2.surfconext.nl/oidc/token",
"registration_endpoint_uri": "",
"user_info_endpoint_uri": "",
"https_required": true
}
2 changes: 1 addition & 1 deletion app/src/debug/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<string name="app_name">eduID DEBUG</string>
<string name="app_name" translatable="false">eduID DEBUG</string>

</resources>
83 changes: 83 additions & 0 deletions app/src/main/kotlin/nl/eduid/di/assist/AuthenticationAssistant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package nl.eduid.di.assist

import android.net.Uri
import net.openid.appauth.*
import net.openid.appauth.connectivity.DefaultConnectionBuilder
import nl.eduid.screens.oauth.Configuration
import timber.log.Timber
import kotlin.coroutines.suspendCoroutine


class AuthenticationAssistant {

suspend fun retrieveOpenIdDiscoveryDoc(configuration: Configuration): AuthorizationServiceConfiguration =
suspendCoroutine { continuation ->
AuthorizationServiceConfiguration.fetchFromUrl(
configuration.discoveryUri ?: Uri.EMPTY,
{ config: AuthorizationServiceConfiguration?, ex: AuthorizationException? ->
when {
ex != null -> {
Timber.e(ex, "Failed to retrieve discovery document")
continuation.resumeWith(Result.failure(ex))
}
config != null -> {
continuation.resumeWith(Result.success(config))
}
else -> {
continuation.resumeWith(Result.failure(RuntimeException("Could not complete discovery")))
}
}
},
DefaultConnectionBuilder.INSTANCE
)
}

suspend fun performRegistrationRequest(
registrationRequest: RegistrationRequest, service: AuthorizationService
): RegistrationResponse = suspendCoroutine { continuation ->
service.performRegistrationRequest(
registrationRequest
) { response: RegistrationResponse?, ex: AuthorizationException? ->
when {
ex != null -> {
Timber.e(ex, "Failed to dynamically register client")
continuation.resumeWith(Result.failure(ex))
}
response != null -> {
continuation.resumeWith(Result.success(response))
}
else -> {
continuation.resumeWith(Result.failure(RuntimeException("Could not complete client dynamic registration")))
}
}

}
}

suspend fun exchangeAuthorizationCode(
response: AuthorizationResponse,
clientAuthentication: ClientAuthentication,
service: AuthorizationService
): TokenResponse = suspendCoroutine { continuation ->
service.performTokenRequest(
/* request = */
response.createTokenExchangeRequest(),
/* clientAuthentication = */
clientAuthentication,
) { tokenResponse, ex ->
when {
ex != null -> {
Timber.e(ex, "Failed to exchange authorization code")
continuation.resumeWith(Result.failure(ex))
}
tokenResponse != null -> {
continuation.resumeWith(Result.success(tokenResponse))
}
else -> {
continuation.resumeWith(Result.failure(RuntimeException("Could not complete client dynamic registration")))
}
}
}
}

}
28 changes: 19 additions & 9 deletions app/src/main/kotlin/nl/eduid/di/module/EduIdModule.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package nl.eduid.di.module

import android.content.Context
import com.squareup.moshi.Moshi
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import nl.eduid.di.api.EduIdApi
import nl.eduid.di.assist.AuthenticationAssistant
import nl.eduid.di.repository.EduIdRepository
import nl.eduid.di.repository.StorageRepository
import okhttp3.OkHttpClient
import org.tiqr.data.api.response.ApiResponseAdapterFactory
import retrofit2.Retrofit
Expand All @@ -34,27 +38,33 @@ internal object RepositoryModule {
api: EduIdApi,
) = EduIdRepository(api)

@Provides
@Singleton
internal fun providesStorageRepository(
@ApplicationContext context: Context,
) = StorageRepository(context)

@Provides
@Singleton
internal fun providesAuthenticationAssist(
) = AuthenticationAssistant()

@Provides
@Singleton
internal fun provideApiRetrofit(
client: Lazy<OkHttpClient>,
moshi: Moshi
client: Lazy<OkHttpClient>, moshi: Moshi
): Retrofit {
return Retrofit.Builder()
.callFactory { client.get().newCall(it) }
return Retrofit.Builder().callFactory { client.get().newCall(it) }
.addCallAdapterFactory(ApiResponseAdapterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl("https://login.eduid.nl/")
.build()
.baseUrl("https://login.eduid.nl/").build()
}

@Provides
@Singleton
internal fun provideOkHttpClientBuilder(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.build()
return OkHttpClient.Builder().connectTimeout(15, TimeUnit.SECONDS).build()
}

}
82 changes: 82 additions & 0 deletions app/src/main/kotlin/nl/eduid/di/repository/StorageRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package nl.eduid.di.repository

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationRequest
import timber.log.Timber
import java.io.IOException

class StorageRepository(private val context: Context) {

private val Context.dataStore by preferencesDataStore(
name = "oauth_settings"
)

val authState: Flow<AuthState?> = context.dataStore.data.catch { exception ->
if (exception is IOException) {
Timber.e(exception, "Error reading preferences.")
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
val authState = preferences[PreferencesKeys.CURRENT_AUTHSTATE]
authState?.let {
AuthState.jsonDeserialize(it)
}
}

val authRequest: Flow<AuthorizationRequest?> = context.dataStore.data.catch { exception ->
if (exception is IOException) {
Timber.e(exception, "Error reading preferences.")
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
val authRequest = preferences[PreferencesKeys.CURRENT_AUTHREQUEST]
authRequest?.let {
AuthorizationRequest.jsonDeserialize(it)
}
}

val clientId: Flow<String?> = context.dataStore.data.catch { exception ->
if (exception is IOException) {
Timber.e(exception, "Error reading preferences.")
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
preferences[PreferencesKeys.CLIENT_ID]
}

suspend fun saveCurrentAuthState(authState: AuthState?) = context.dataStore.edit { settings ->
settings[PreferencesKeys.CURRENT_AUTHSTATE] =
authState?.jsonSerializeString() ?: settings.remove(PreferencesKeys.CURRENT_AUTHSTATE)
}

suspend fun saveCurrentAuthRequest(authRequest: AuthorizationRequest?) =
context.dataStore.edit { settings ->
settings[PreferencesKeys.CURRENT_AUTHREQUEST] = authRequest?.jsonSerializeString()
?: settings.remove(PreferencesKeys.CURRENT_AUTHREQUEST)
}

suspend fun saveClientId(clientId: String) = context.dataStore.edit { settings ->
settings[PreferencesKeys.CLIENT_ID] = clientId
}

private object PreferencesKeys {
val CURRENT_AUTHSTATE = stringPreferencesKey("current_authstate")
val CURRENT_AUTHREQUEST = stringPreferencesKey("current_authrequest")
val CLIENT_ID = stringPreferencesKey("currentClientId")
}

}
32 changes: 32 additions & 0 deletions app/src/main/kotlin/nl/eduid/screens/oauth/Configuration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nl.eduid.screens.oauth

import android.net.Uri

data class Configuration(
val clientId: String?,
val scope: String,
val redirectUri: Uri,
val endSessionRedirectUri: Uri,
val discoveryUri: Uri?,
val authEndpointUri: Uri,
val tokenEndpointUri: Uri,
val endSessionEndpoint: Uri,
val registrationEndpointUri: Uri,
val isHttpsRequired: Boolean
) {
companion object {
val EMPTY = Configuration(
clientId = "",
scope = "",
redirectUri = Uri.EMPTY,
endSessionRedirectUri = Uri.EMPTY,
discoveryUri = Uri.EMPTY,
authEndpointUri = Uri.EMPTY,
tokenEndpointUri = Uri.EMPTY,
endSessionEndpoint = Uri.EMPTY,
registrationEndpointUri = Uri.EMPTY,
isHttpsRequired = true

)
}
}
24 changes: 24 additions & 0 deletions app/src/main/kotlin/nl/eduid/screens/oauth/OAuthContract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package nl.eduid.screens.oauth


import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import timber.log.Timber


class OAuthContract(
) : ActivityResultContract<OAuthViewModel, Intent?>() {

override fun createIntent(context: Context, viewModel: OAuthViewModel): Intent = try {
viewModel.createAuthorizationIntent()
} catch (e: Exception) {
Timber.e(e, "Failed to create authorization request intent")
Intent()
}


override fun parseResult(resultCode: Int, intent: Intent?): Intent? {
return intent
}
}
Loading

0 comments on commit dd25e45

Please sign in to comment.