Skip to content

Commit

Permalink
Merge pull request #12 from Liftric/feature/parse-id-token
Browse files Browse the repository at this point in the history
feat(api): Get Claims from Id Token
  • Loading branch information
gaebel authored Sep 8, 2020
2 parents 3eb24fd + e06578a commit 16bf9d0
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 4 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,17 @@ Returns the users attributes and metadata on success.
More info about this in the [official documentation](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_GetUser.html).

```kotlin
getUser(accessToken = "TOKEN_FROM_SIGN_IN_REQUEST"): GetUserResponse
getUser(accessToken = "TOKEN_FROM_SIGN_IN_REQUEST"): Result<GetUserResponse>
```

#### Get Claims

Parses the ID token to a Claims object (e.g. to access the sub id or email address).

> Not generic, refer to the Claims class to see which parameters are supported.
```kotlin
getClaims(fromIdToken = "ID_TOKEN_FROM_SIGN_IN_REQUEST"): Result<Claims>
```

#### Update User Attributes
Expand Down Expand Up @@ -148,12 +158,12 @@ confirmForgotPassword(confirmationCode = "CODE_FROM_DELIVERY_MEDIUM", username =
Gets the user attribute verification code for the specified attribute name

```kotlin
getUserAttributeVerificationCode(accessToken = "TOKEN_FROM_SIGN_IN_REQUEST", attributeName = "EMAIL", clientMetadata = null): Result<UpdateUserAttributesResponse>
getUserAttributeVerificationCode(accessToken = "TOKEN_FROM_SIGN_IN_REQUEST", attributeName = "EMAIL", clientMetadata = null): Result<GetAttributeVerificationCodeResponse>
```

#### Verify User Attribute

Verifies the specified user attribute
Verifies the specified user attribute.

```kotlin
verifyUserAttribute(accessToken = "TOKEN_FROM_SIGN_IN_REQUEST", attributeName = "EMAIL", code = "CODE_FROM_DELIVERY_MEDIUM"): Result<Unit>
Expand Down
9 changes: 9 additions & 0 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ kotlin {
}
val androidTest by getting {
dependencies {
implementation(TestLibs.RoboElectrics) {
exclude(Exclude.GoogleAutoService, Exclude.AutoService)
}
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
implementation(TestLibs.TestCore)
Expand Down Expand Up @@ -67,6 +70,12 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

testOptions {
unitTests.apply {
isReturnDefaultValues = true
}
}
}

val artifactName = "Auth"
Expand Down
11 changes: 11 additions & 0 deletions auth/src/androidMain/kotlin/com/liftric/auth/base/Base64.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.liftric.auth.base

import android.util.Base64

internal actual class Base64 {
actual companion object {
actual fun decode(string: String): String? {
return String(Base64.decode(string, Base64.URL_SAFE), Charsets.UTF_8)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.liftric.auth

import androidx.test.core.app.ApplicationProvider
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
actual class AuthHandlerIntegrationTests: AbstractAuthHandlerIntegrationTests()
7 changes: 7 additions & 0 deletions auth/src/commonMain/kotlin/com/liftric/auth/Auth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,11 @@ interface Auth {
* @return Result object containing Unit on success or an error on failure
*/
suspend fun deleteUser(accessToken: String): Result<Unit>

/**
* Parses the id token and returns the claims (Not all claims implemented!)
* @param fromIdToken The id token from the sign in request
* @return Result object containing Claims on success or an error on failure
*/
fun getClaims(fromIdToken: String): Result<Claims>
}
17 changes: 17 additions & 0 deletions auth/src/commonMain/kotlin/com/liftric/auth/AuthHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.core.*
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json

/**
* AWS Cognito authentication client
Expand Down Expand Up @@ -260,6 +261,22 @@ open class AuthHandler(private val configuration: Configuration) : Auth {
}
}

override fun getClaims(fromIdToken: String): Result<Claims> {
return try {
val component = fromIdToken.split(".")[1]
Base64.decode(component)?.let { decoded64 ->
val json = Json { encodeDefaults = true; isLenient = true }
json.decodeFromString(Claims.serializer(), decoded64)?.let { claims ->
Result.success(claims)
}
}?: run {
Result.failure(Error("Couldn't decode JWT token"))
}
} catch (e: Exception) {
Result.failure(e)
}
}

//----------
// REQUEST
//----------
Expand Down
7 changes: 7 additions & 0 deletions auth/src/commonMain/kotlin/com/liftric/auth/base/Base64.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.liftric.auth.base

internal expect class Base64 {
companion object {
fun decode(string: String): String?
}
}
23 changes: 23 additions & 0 deletions auth/src/commonMain/kotlin/com/liftric/auth/base/Response.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.liftric.auth.base

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
Expand Down Expand Up @@ -62,4 +63,26 @@ data class GetAttributeVerificationCodeResponse(
@Serializable
data class ForgotPasswordResponse(
val CodeDeliveryDetails: CodeDeliveryDetails = CodeDeliveryDetails()
)

@Serializable
data class Claims(
val sub: String,
val aud: String,
@SerialName("cognito:groups")
val cognitoGroups: List<String>,
@SerialName("email_verified")
val emailVerified: Boolean? = null,
@SerialName("event_id")
val eventId: String,
@SerialName("token_use")
val tokenUse: String,
@SerialName("auth_time")
val authTime: String,
val iss: String,
@SerialName("cognito:username")
val cognitoUsername: String,
val exp: String,
val iat: String,
val email: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import kotlin.test.*
import kotlinx.coroutines.*
import kotlin.js.JsName

class AuthHandlerIntegrationTests() {
expect class AuthHandlerIntegrationTests: AbstractAuthHandlerIntegrationTests
abstract class AbstractAuthHandlerIntegrationTests() {
private val configuration = Configuration(
Environment.variable("origin") ?: "",
Region.euCentral1,
Expand Down Expand Up @@ -255,4 +256,36 @@ class AuthHandlerIntegrationTests() {
assertNull(updateUserAttributesResponse.getOrNull())
assertEquals("Invalid Access Token", updateUserAttributesResponse.exceptionOrNull()!!.message)
}

@JsName("GetClaimsWithoutEmailTest")
@Test
fun `Test if get claims works without email address`() {
val token = "eyJraWQiOiJwREgwTUpqeWdoRk4wT2J1cFpUNzl1QytLZkpZQ3BtNnZTamVXb3NpZUlFPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxZWEyMTg1Yi1iYTk5LTRlYjUtYmZkMS0yMDY0MjQ2Yzc0NWQiLCJhdWQiOiIzdjRzNm9lMmRobjZua2hydTU3OWc2bTZnMSIsImNvZ25pdG86Z3JvdXBzIjpbIlJPTEVfUEFUSUVOVCJdLCJldmVudF9pZCI6IjA3ODE5NGUzLTM5YzQtNDBhYS04MzkwLTkzMmI2MjY2MjE3YSIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNTk5NTY1OTU4LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuZXUtY2VudHJhbC0xLmFtYXpvbmF3cy5jb21cL2V1LWNlbnRyYWwtMV9DMUduN0hiWU4iLCJjb2duaXRvOnVzZXJuYW1lIjoiNDMzMGI4ZjctYmRjMy00MTI2LTllYWUtZWVkZTc0ZTI0MTEyIiwiZXhwIjoxNTk5NTY5NTU4LCJpYXQiOjE1OTk1NjU5NTh9.hLzDOItbHkUWYyI9hTFIKkoC50_UrRnPFoIcyrsiCzP5zQFlhTboe9TZ6BE0o21IEk8tcdBkFRHbuM8zPru-qopB9tC7pkhvY1FoPMlNlRSmqj8YZjJ8InnHForkdJ4n9keM8PcdwW6KWlAjLViwSmOl3k-ptQq1DnmnmGmcmfzssDzON0R__jsyEQs_EZWQ3c86qodbIU4peN9Dm26TMSQCzJhZwvCuGRmRgplsqOD4UfDVh5ya-bXUogJuirlE8KFUH1my13AJOxAJLBgyOjBZceFMnC4ZqZBbSND-iiMvQcpn4O6gd5p5Je367LO56w_ypo6eMffEs39fikIP4g"
authHandler.getClaims(token)
.onSuccess { claims ->
assertNotNull(claims)
assertEquals(claims.sub, "1ea2185b-ba99-4eb5-bfd1-2064246c745d")
assertEquals(claims.email, null)
assertEquals(claims.emailVerified, null)
}
.onFailure {
fail(it.message)
}
}

@JsName("GetClaimsWithEmailTest")
@Test
fun `Test if get claims works with email address`() {
val token = "eyJraWQiOiJwREgwTUpqeWdoRk4wT2J1cFpUNzl1QytLZkpZQ3BtNnZTamVXb3NpZUlFPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI3NTUzZGRmOC1hMTAzLTRjYjItOWVkZi0yNDcwMTBmNGNjNGQiLCJhdWQiOiIzdjRzNm9lMmRobjZua2hydTU3OWc2bTZnMSIsImNvZ25pdG86Z3JvdXBzIjpbIlJPTEVfUEFUSUVOVCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImV2ZW50X2lkIjoiZmMxNTM3NTQtNDY5ZS00YzZiLTlhMzktODVhM2M3MDAxZTMwIiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE1OTk1NjY5MjMsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC5ldS1jZW50cmFsLTEuYW1hem9uYXdzLmNvbVwvZXUtY2VudHJhbC0xX0MxR243SGJZTiIsImNvZ25pdG86dXNlcm5hbWUiOiI2YTg0MzYzNS1kZWM2LTQxMmYtYjI0MS1iNGRmYmI2NTVkM2YiLCJleHAiOjE1OTk1NzA1MjMsImlhdCI6MTU5OTU2NjkyMywiZW1haWwiOiJnYWViZWxAbGlmdHJpYy5jb20ifQ.ka1nCmT-ACwbvQ3uy3qsuZII6PQzdfJHA7UY3Wkt_7GU2fxBxcDdRjzdDdCmh4IE0e0uwfoddMXTXWaijo6yKvrv0VHtfsIkfFJb09TNtCNrxTy1PX-bJNeVT752N85pdNpkms6GefylP2iAZec520ISI1ZrHz0jlKfUq6iGpq3GKxIXJZ_dQGVPa2oTQDqG_CmOsr9sTRl8EoMoEIjxJdOAFeYltlPDcuhWZVUWsfwUq290UdOTBJhGruIre-cdfe03FEo9NG67mewldRYdsjNBgGQU_Jyp68hg1UQHrhKC-eUDmrWiyYGzKwbkUCCm1puwcy_wpu5HRQfjAjVW4A"
authHandler.getClaims(token)
.onSuccess { claims ->
assertNotNull(claims)
assertEquals(claims.sub, "7553ddf8-a103-4cb2-9edf-247010f4cc4d")
assertNotEquals(claims.email, null)
assertEquals(claims.emailVerified, false)
}
.onFailure {
fail(it.message)
}
}
}
23 changes: 23 additions & 0 deletions auth/src/iosMain/kotlin/com/liftric/auth/base/Base64.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.liftric.auth.base

import platform.Foundation.NSData
import platform.Foundation.NSString
import platform.Foundation.NSUTF8StringEncoding
import platform.Foundation.create

internal actual class Base64 {
actual companion object {
actual fun decode(string: String): String? {
var encoded64 = string
val remainder = encoded64.count() % 4
if (remainder > 0) {
encoded64 = encoded64.padEnd(string.count() + (4 - remainder), '=')
}
return NSData.create(encoded64, 0)?.let {
(NSString.create(it, NSUTF8StringEncoding) as String?)
}?: run {
null
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.liftric.auth

actual class AuthHandlerIntegrationTests: AbstractAuthHandlerIntegrationTests()
11 changes: 11 additions & 0 deletions buildSrc/src/main/java/Libs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ object Apps {
const val versionName = "1.0.0"
}

object Android {
const val TestRunner = "org.robolectric.RobolectricTestRunner"
}

object Versions {
const val gradle = "4.0.1"
const val kotlin = "1.4.0"
const val coroutines = "1.3.9-native-mt"
const val serialization = "1.0.0-RC"
const val ktor = "1.4.0"
const val TestCore = "1.2.0"
const val RoboElectric = "4.3.1"
}

object Libs {
Expand All @@ -36,4 +41,10 @@ object Libs {

object TestLibs {
const val TestCore = "androidx.test:core:${Versions.TestCore}"
const val RoboElectrics = "org.robolectric:robolectric:${Versions.RoboElectric}"
}

object Exclude {
const val GoogleAutoService = "com.google.auto.service"
const val AutoService = "auto-service"
}

0 comments on commit 16bf9d0

Please sign in to comment.