Skip to content

Commit

Permalink
Merge pull request #14 from Liftric/feature/proper-token-payload-access
Browse files Browse the repository at this point in the history
feat(jwt): serialize tokens, fixes #13
  • Loading branch information
benjohnde authored Sep 28, 2020
2 parents 16bf9d0 + 7c49a94 commit 73f47b8
Show file tree
Hide file tree
Showing 15 changed files with 774 additions and 93 deletions.
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,24 +95,33 @@ At the moment you can only sign in with username and password.
signIn(username = "USERNAME", password = "PASSWORD"): Result<SignInResponse>
```

#### Get User
#### Get Claims

Returns the users attributes and metadata on success.
You can retrieve the claims of both the IdTokens' and AccessTokens' payload by converting them to either a `CognitoIdToken` or `CognitoAccessToken`

More info about this in the [official documentation](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_GetUser.html).
```kotlin
val idToken = CognitoIdToken(idTokenString)
// or
val accessToken = CognitoAccessToken(accessTokenString)

val phoneNumber = idToken.claims.phoneNumber
val sub = idToken.claims.sub
```

Custom attributes of the IdToken get mapped into `customAttributes`

```kotlin
getUser(accessToken = "TOKEN_FROM_SIGN_IN_REQUEST"): Result<GetUserResponse>
val twitter = idToken.claims.customAttributes["custom:twitter"]
```

#### Get Claims
#### Get User

Parses the ID token to a Claims object (e.g. to access the sub id or email address).
Returns the users attributes and metadata on success.

> Not generic, refer to the Claims class to see which parameters are supported.
More info about this in the [official documentation](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_GetUser.html).

```kotlin
getClaims(fromIdToken = "ID_TOKEN_FROM_SIGN_IN_REQUEST"): Result<Claims>
getUser(accessToken = "TOKEN_FROM_SIGN_IN_REQUEST"): Result<GetUserResponse>
```

#### Update User Attributes
Expand Down
11 changes: 0 additions & 11 deletions auth/src/androidMain/kotlin/com/liftric/auth/base/Base64.kt

This file was deleted.

16 changes: 16 additions & 0 deletions auth/src/androidMain/kotlin/com/liftric/auth/jwt/Base64.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.liftric.auth.jwt

import android.util.Base64
import java.io.UnsupportedEncodingException

internal actual class Base64 {
actual companion object {
actual fun decode(string: String): String? {
return try {
String(Base64.decode(string, Base64.URL_SAFE), Charsets.UTF_8)
} catch (e: Exception) {
null
}
}
}
}
11 changes: 4 additions & 7 deletions auth/src/commonMain/kotlin/com/liftric/auth/Auth.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.liftric.auth

import com.liftric.auth.base.*
import com.liftric.auth.jwt.Base64
import com.liftric.auth.jwt.CognitoAccessToken
import com.liftric.auth.jwt.CognitoIdToken
import kotlinx.serialization.json.Json

interface Auth {
/**
Expand Down Expand Up @@ -100,11 +104,4 @@ 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>
}
20 changes: 4 additions & 16 deletions auth/src/commonMain/kotlin/com/liftric/auth/AuthHandler.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.liftric.auth

import com.liftric.auth.base.*
import com.liftric.auth.jwt.Base64
import com.liftric.auth.jwt.CognitoIdToken
import com.liftric.auth.jwt.CognitoAccessToken
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.request.*
Expand All @@ -9,6 +12,7 @@ import io.ktor.http.*
import io.ktor.utils.io.core.*
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlin.reflect.KClass

/**
* AWS Cognito authentication client
Expand Down Expand Up @@ -261,22 +265,6 @@ 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
22 changes: 0 additions & 22 deletions auth/src/commonMain/kotlin/com/liftric/auth/base/Response.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,4 @@ 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
)
87 changes: 87 additions & 0 deletions auth/src/commonMain/kotlin/com/liftric/auth/jwt/AccessToken.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.liftric.auth.jwt

/**
* Access Token containing claims specified by IETF:
* https://tools.ietf.org/html/rfc7519#section-4
*/
interface AccessToken {
/**
* Audience
*/
val aud: String?

/**
* Expiration Time
*/
val exp: Long

/**
* Issued at
*/
val iat: Long

/**
* Issuer
*/
val iss: String

/**
* JWT ID
*/
val jti: String

/**
* Not Before
*/
val nbf: Long?

/**
* Subject
*/
val sub: String
}

/**
* Access Token extension for Cognito
*/
interface AccessTokenExtension {
/**
* Time when the authentication occurred. JSON number that represents the number of seconds from 1970-01-01T0:0:0Z as measured in UTC format
*/
val authTime: Long

/**
* Client id
*/
val clientId: String

/**
* List of groups the user belongs to
*/
val cognitoGroups: List<String>

/**
* Device key
*/
val deviceKey: String?

/**
* Event id
*/
val eventId: String?

/**
* List of Oauth 2.0 scopes that define what access the token provides
*/
val scope: String?

/**
* Intended purpose of this token. Its value is always access
*/
val tokenUse: String

/**
* Username
*/
val username: String
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.liftric.auth.base
package com.liftric.auth.jwt

internal expect class Base64 {
companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.liftric.auth.jwt

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

class InvalidCognitoAccessTokenException(message:String): Exception(message)

@Serializable
data class CognitoAccessTokenClaims(
override val aud: String? = null,
override val exp: Long,
override val iat: Long,
override val iss: String,
override val jti: String,
override val nbf: Long? = null,
override val sub: String,
@SerialName("auth_time")
override val authTime: Long,
@SerialName("client_id")
override val clientId: String,
@SerialName("cognito:groups")
override val cognitoGroups: List<String>,
@SerialName("device_key")
override val deviceKey: String? = null,
@SerialName("event_id")
override val eventId: String? = null,
override val scope: String? = null,
@SerialName("token_use")
override val tokenUse: String,
override val username: String
): AccessToken, AccessTokenExtension

class CognitoAccessToken(accessTokenString: String): JWT<CognitoAccessTokenClaims>(accessTokenString) {
override val claims: CognitoAccessTokenClaims
get() {
try {
return Json.decodeFromString(CognitoAccessTokenClaims.serializer(), getPayload())
} catch (e: Exception) {
throw InvalidCognitoAccessTokenException("This is not a valid access token")
}
}
}
52 changes: 52 additions & 0 deletions auth/src/commonMain/kotlin/com/liftric/auth/jwt/CognitoIdToken.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.liftric.auth.jwt

import kotlinx.serialization.*
import kotlinx.serialization.json.Json

class InvalidCognitoIdTokenException(message:String): Exception(message)

@Serializable(with = CustomAttributesSerializer::class)
data class CognitoIdTokenClaims(
override val sub: String? = null,
override val name: String? = null,
override val givenName: String? = null,
override val familyName: String? = null,
override val middleName: String? = null,
override val nickname: String? = null,
override val preferredUsername: String? = null,
override val profile: String? = null,
override val picture: String? = null,
override val website: String? = null,
override val email: String? = null,
override val emailVerified: Boolean? = null,
override val gender: String? = null,
override val birthdate: String? = null,
override val zoneinfo: String? = null,
override val locale: String? = null,
override val phoneNumber: String? = null,
override val phoneNumberVerified: Boolean? = null,
override val address: Address? = null,
override val updatedAt: Long? = null,
override val aud: String,
override val authTime: Long,
override val cognitoGroups: List<String>,
override val cognitoUsername: String,
override val exp: Long,
override val eventId: String,
override val iss: String,
override val iat: Long,
override val scope: String? = null,
override val tokenUse: String,
override val customAttributes: Map<String, String>? = null
): IdToken, IdTokenExtension

class CognitoIdToken(idTokenString: String): JWT<CognitoIdTokenClaims>(idTokenString) {
override val claims: CognitoIdTokenClaims
get() {
try {
return Json.decodeFromString(CognitoIdTokenClaims.serializer(), getPayload())
} catch (e: SerializationException) {
throw InvalidCognitoIdTokenException("This is not a valid cognito id token")
}
}
}
Loading

0 comments on commit 73f47b8

Please sign in to comment.