diff --git a/credentials/src/main/java/me/uport/sdk/credentials/ClaimsRequestParams.kt b/credentials/src/main/java/me/uport/sdk/credentials/ClaimsRequestParams.kt new file mode 100644 index 00000000..d03a8aea --- /dev/null +++ b/credentials/src/main/java/me/uport/sdk/credentials/ClaimsRequestParams.kt @@ -0,0 +1,149 @@ +package me.uport.sdk.credentials + +/** + * This is a helper class for building verifiable claim requests + * [specs](https://github.com/uport-project/specs/blob/develop/messages/sharereq.md#claims-spec) + * It supports adding multiple verifiable and user-info parameters and returns a map which can be + * added to the selective disclosure request + * + */ +class ClaimsRequestParams { + + private val verifiableMap: MutableMap = mutableMapOf() + private val userInfoMap: MutableMap = mutableMapOf() + + /** + * This adds a unique record in the verifiable object + * The [type] param for this method is used as the key for the verifiable + * Values will be overwritten if this method is called more than once with the same type + * + */ + fun addVerifiable(type: String, params: VerifiableParams?): ClaimsRequestParams { + verifiableMap.put(type, params?.getMap()) + return this + } + + /** + * This adds a unique record in the user_info object + * The [type] param for this method is used as the key for the user_info + * Values will be overwritten if this method is called more than once with the same type + * + */ + fun addUserInfo(type: String, params: UserInfoParams?): ClaimsRequestParams { + userInfoMap.put(type, params?.getMap()) + return this + } + + /** + * Returns the final map which can be used in a selective disclosure request + * + */ + fun build(): Map { + val payload: MutableMap = mutableMapOf() + payload["verifiable"] = verifiableMap + payload["user_info"] = userInfoMap + return payload + } +} + +/** + * This is a helper class for adding properties and values to a verifiable + * + */ +data class VerifiableParams( + + /** + * [**optional**] + * Short string explaining why you need this + * + */ + private val reason: String? = null, + + /** + * [**optional**] + * Indicate if this claim is essential + * + */ + private val essential: Boolean = false +) { + + private val params: MutableMap = mutableMapOf() + private val issuers: MutableList = mutableListOf() + + /** + * This adds records to the array of issuers + * + */ + fun addIssuer(did: String, url: String? = null): VerifiableParams { + issuers.add(Issuer(did, url)) + return this + } + + /** + * Returns a Mutable map of the a verifiable's params + * + */ + fun getMap(): MutableMap { + params["iss"] = issuers + params["reason"] = reason + params["essential"] = essential + return params + } +} + +/** + * This is a helper class for adding properties and values to user_info + * + */ +data class UserInfoParams( + + /** + * [**optional**] + * Short string explaining why you need this + * + */ + private val reason: String? = null, + + /** + * [**optional**] + * Indicate if this claim is essential + * + */ + private val essential: Boolean = false + +) { + + private val params: MutableMap = mutableMapOf() + + /** + * Returns a Mutable map of the user_info params + * + */ + fun getMap(): MutableMap { + params["reason"] = reason + params["essential"] = essential + return params + } +} + + +/** + * This is a helper class for adding properties and values to issuer + * + */ +data class Issuer( + + /** + * [**required**] + * The DID of allowed issuer of claims + * + */ + private val did: String, + + /** + * [**optional**] + * The URL for obtaining the claim + * + */ + private val url: String? = null +) \ No newline at end of file diff --git a/credentials/src/main/java/me/uport/sdk/credentials/SelectiveDisclosureRequestParams.kt b/credentials/src/main/java/me/uport/sdk/credentials/SelectiveDisclosureRequestParams.kt index 93402007..c5e8297b 100644 --- a/credentials/src/main/java/me/uport/sdk/credentials/SelectiveDisclosureRequestParams.kt +++ b/credentials/src/main/java/me/uport/sdk/credentials/SelectiveDisclosureRequestParams.kt @@ -2,11 +2,7 @@ package me.uport.sdk.credentials -import me.uport.sdk.credentials.RequestAccountType.devicekey -import me.uport.sdk.credentials.RequestAccountType.general -import me.uport.sdk.credentials.RequestAccountType.keypair -import me.uport.sdk.credentials.RequestAccountType.none -import me.uport.sdk.credentials.RequestAccountType.segregated +import me.uport.sdk.credentials.RequestAccountType.* /** * A class that encapsulates the supported parameter types for creating a SelectiveDisclosureRequest. @@ -19,78 +15,89 @@ import me.uport.sdk.credentials.RequestAccountType.segregated * and ease of use of frequently used params. */ class SelectiveDisclosureRequestParams( - /** - * [**required**] - * a simple_list of attributes for which you are requesting credentials. - * Ex. [ 'name', 'country' ] - */ - val requested: List, - - /** - * [**required**] - * the url that can receive the response to this request. - * TODO: detail how that URL should be handled by the APP implementing this SDK - * - * This gets encoded as `callback` in the JWT payload - */ - val callbackUrl: String, - - /** - * [**optional**] - * A simple_list of signed claims being requested. - * This is semantically similar to the [requested] field - * but the response should contain signatures as well. - */ - val verified: List? = null, - - /** - * [**optional**] - * The Ethereum network ID if it is relevant for this request. - * - * This gets encoded as `net` in the JWT payload - * - * Examples: `"0x4"`, [Networks.mainnet.networkId] - */ - val networkId: String? = null, - - - /** - * [**optional**] - * If this request implies a particular kind of account. - * This defaults to [RequestAccountType.general] (user choice) - * - * This gets encoded as `act` in the JWT payload - * - * @see [RequestAccountType] - */ - val accountType: RequestAccountType? = general, - - /** - * [**optional**] - * A list of signed claims about the issuer, usually signed by 3rd parties. - */ - val vc: List? = null, - - /** - * [**optional**] defaults to [DEFAULT_SHARE_REQ_VALIDITY_SECONDS] - * The validity interval of this request, measured in seconds since the moment it is issued. - */ - val expiresInSeconds: Long? = DEFAULT_SHARE_REQ_VALIDITY_SECONDS, - - - //omitting the "notifications" permission because it has no relevance on android. - // It may be worth adding for direct interop with iOS but that is unclear now - - /** - * [**optional**] - * This can hold extra fields for the JWT payload representing the request. - * Use this to provide any of the extra fields described in the - * [specs](https://github.com/uport-project/specs/blob/develop/messages/sharereq.md) - * - * The fields contained in [extras] will get overwritten by the named parameters - * in this class in case of a name collision. - */ - val extras: Map? = null + /** + * [**required**] + * a simple_list of attributes for which you are requesting credentials. + * Ex. [ 'name', 'country' ] + */ + val requested: List, + + /** + * [**required**] + * the url that can receive the response to this request. + * TODO: detail how that URL should be handled by the APP implementing this SDK + * + * This gets encoded as `callback` in the JWT payload + */ + val callbackUrl: String, + + /** + * [**optional**] + * A simple_list of signed claims being requested. + * This is semantically similar to the [requested] field + * but the response should contain signatures as well. + */ + val verified: List? = null, + + /** + * [**optional**] + * This allows you to request claims with very specific properties. + * This replaces the [requested] and [verified] parameters of this request. + * You may still include requested and verified to provide support for older clients. + * But they will be ignored if by newer clients if the claims field is present + * [specs](https://github.com/uport-project/specs/blob/develop/messages/sharereq.md#claims-spec) + * + */ + val claims: Map? = null, + + /** + * [**optional**] + * The Ethereum network ID if it is relevant for this request. + * + * This gets encoded as `net` in the JWT payload + * + * Examples: `"0x4"`, [Networks.mainnet.networkId] + */ + val networkId: String? = null, + + + /** + * [**optional**] + * If this request implies a particular kind of account. + * This defaults to [RequestAccountType.general] (user choice) + * + * This gets encoded as `act` in the JWT payload + * + * @see [RequestAccountType] + */ + val accountType: RequestAccountType? = general, + + /** + * [**optional**] + * A list of signed claims about the issuer, usually signed by 3rd parties. + */ + val vc: List? = null, + + /** + * [**optional**] defaults to [DEFAULT_SHARE_REQ_VALIDITY_SECONDS] + * The validity interval of this request, measured in seconds since the moment it is issued. + */ + val expiresInSeconds: Long? = DEFAULT_SHARE_REQ_VALIDITY_SECONDS, + + + //omitting the "notifications" permission because it has no relevance on android. + // It may be worth adding for direct interop with iOS but that is unclear now + + /** + * [**optional**] + * This can hold extra fields for the JWT payload representing the request. + * Use this to provide any of the extra fields described in the + * [specs](https://github.com/uport-project/specs/blob/develop/messages/sharereq.md) + * + * The fields contained in [extras] will get overwritten by the named parameters + * in this class in case of a name collision. + */ + val extras: Map? = null ) /** @@ -121,6 +128,7 @@ internal fun buildPayloadForShareReq(params: SelectiveDisclosureRequestParams): val payload = params.extras.orEmpty().toMutableMap() payload["callback"] = params.callbackUrl + payload["claims"] = params.claims.orEmpty().toMutableMap() payload["requested"] = params.requested params.verified?.let { payload["verified"] = it } params.vc?.let { payload["vc"] = it } diff --git a/credentials/src/test/java/me/uport/sdk/credentials/CredentialsTest.kt b/credentials/src/test/java/me/uport/sdk/credentials/CredentialsTest.kt index aa883466..5e4b5ebe 100644 --- a/credentials/src/test/java/me/uport/sdk/credentials/CredentialsTest.kt +++ b/credentials/src/test/java/me/uport/sdk/credentials/CredentialsTest.kt @@ -566,4 +566,124 @@ class CredentialsTest { isInstanceOf(JWTAuthenticationException::class) } } + + @Test + fun `claims request params builds request successfully`() = runBlocking { + val claim = ClaimsRequestParams() + .addVerifiable( + "email", + VerifiableParams( + "We need to be able to email you", + true + ) + .addIssuer("did:web:uport.claims", "https://uport.claims/email") + .addIssuer("did:web:sobol.io", "https://sobol.io/verify") + ) + .addVerifiable( + "nationalIdentity", + VerifiableParams( + "To legally be able to open your account" + ) + .addIssuer("did:web:idverifier.claims", "https://idverifier.example") + ) + .addUserInfo("name", UserInfoParams("Show your name to other users", true)) + .addUserInfo("country", UserInfoParams("Show your country to other users", true)) + .build() + + val params = SelectiveDisclosureRequestParams( + requested = listOf("name", "country"), + callbackUrl = "myapp://get-back-to-me-with-response.url", + claims = claim + ) + + val load = buildPayloadForShareReq(params) + + assertThat((load.get("claims") as Map<*, *>).containsKey("verifiable")).isEqualTo(true) + assertThat((load.get("claims") as Map<*, *>).containsKey("user_info")).isEqualTo(true) + + assertThat(((load.get("claims") as Map<*, *>).get("verifiable") as Map<*, *>).containsKey("email")).isEqualTo( + true + ) + assertThat(((load.get("claims") as Map<*, *>).get("verifiable") as Map<*, *>).containsKey("nationalIdentity")).isEqualTo( + true + ) + + assertThat(((load.get("claims") as Map<*, *>).get("user_info") as Map<*, *>).containsKey("name")).isEqualTo(true) + assertThat(((load.get("claims") as Map<*, *>).get("user_info") as Map<*, *>).containsKey("country")).isEqualTo( + true + ) + } + + @Test + fun `claims request params builds request successfully with minimal params`() = runBlocking { + val claim = ClaimsRequestParams() + .addVerifiable( + "email", + null + ) + .addVerifiable( + "nationalIdentity", + VerifiableParams( + null + ) + .addIssuer("did:web:idverifier.claims") + ) + .addUserInfo("name", UserInfoParams(null)) + .addUserInfo("country", null) + .build() + + val params = SelectiveDisclosureRequestParams( + requested = listOf("name", "country"), + callbackUrl = "myapp://get-back-to-me-with-response.url", + claims = claim + ) + + val load = buildPayloadForShareReq(params) + + assertThat((load.get("claims") as Map<*, *>).containsKey("verifiable")).isEqualTo(true) + assertThat((load.get("claims") as Map<*, *>).containsKey("user_info")).isEqualTo(true) + + assertThat(((load.get("claims") as Map<*, *>).get("verifiable") as Map<*, *>).get("email")).isNull() + assertThat(((load.get("claims") as Map<*, *>).get("user_info") as Map<*, *>).get("country")).isNull() + } + + @Test + fun `can decode a jwt with claims field into a map`() = runBlocking { + val claim = ClaimsRequestParams() + .addVerifiable( + "email", + VerifiableParams( + "We need to be able to email you", + true + ) + .addIssuer("did:web:uport.claims", "https://uport.claims/email") + .addIssuer("did:web:sobol.io", "https://sobol.io/verify") + ) + .addVerifiable( + "nationalIdentity", + VerifiableParams( + "To legally be able to open your account" + ) + .addIssuer("did:web:idverifier.claims", "https://idverifier.example") + ) + .addUserInfo("name", UserInfoParams("Show your name to other users", true)) + .addUserInfo("country", UserInfoParams("Show your country to other users", true)) + .build() + + val params = SelectiveDisclosureRequestParams( + requested = listOf("name", "country"), + callbackUrl = "myapp://get-back-to-me-with-response.url", + claims = claim + ) + + val signer = KPSigner("0x1234") + val issuer = "did:ethr:${signer.getAddress()}" + + val cred = Credentials(issuer, signer) + val requestJWT = cred.createDisclosureRequest(params) + + val (_, payloadMap, _) = JWTTools().decodeRaw(requestJWT) + + assertThat(payloadMap.containsKey("claims")).isEqualTo(true) + } } diff --git a/demoapp/src/main/java/me/uport/sdk/demoapp/request_flows/uPortLoginActivity.kt b/demoapp/src/main/java/me/uport/sdk/demoapp/request_flows/uPortLoginActivity.kt index c7f24b88..fb59a59d 100644 --- a/demoapp/src/main/java/me/uport/sdk/demoapp/request_flows/uPortLoginActivity.kt +++ b/demoapp/src/main/java/me/uport/sdk/demoapp/request_flows/uPortLoginActivity.kt @@ -8,20 +8,15 @@ import kotlinx.android.synthetic.main.activity_uport_login.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import me.uport.sdk.signer.KPSigner import me.uport.sdk.core.UI -import me.uport.sdk.credentials.Credentials -import me.uport.sdk.credentials.SelectiveDisclosureRequestParams +import me.uport.sdk.credentials.* import me.uport.sdk.demoapp.R import me.uport.sdk.jwt.JWTTools -import me.uport.sdk.transport.ErrorUriResponse -import me.uport.sdk.transport.JWTUriResponse -import me.uport.sdk.transport.ResponseParser -import me.uport.sdk.transport.Transports -import me.uport.sdk.transport.UriResponse +import me.uport.sdk.signer.KPSigner +import me.uport.sdk.transport.* /** - * This allows the users initiate a uPort login using [SelectiveDisclosureRequest] + * This allows the users initiate a uPort login using SelectiveDisclosureRequest * and then receive the deeplink response via [onActivityResult] */ class uPortLoginActivity : AppCompatActivity() { @@ -36,11 +31,70 @@ class uPortLoginActivity : AppCompatActivity() { // create a DID val issuerDID = "did:ethr:${signer.getAddress()}" + /*@Suppress("StringLiteralDuplication") + val claim = mapOf( + "verifiable" to mapOf( + "email" to mapOf( + "iss" to listOf( + mapOf( + "did" to "did:web:uport.claims", + "url" to "https://uport.claims/email" + ), + mapOf( + "did" to "did:web:sobol.io", + "url" to "https://sobol.io/verify" + ) + ), + "reason" to "We need to be able to email you", + "essential" to false + ), + "nationalIdentity" to mapOf( + "iss" to listOf( + mapOf( + "did" to "did:web:idverifier.claims", + "url" to "https://idverifier.example" + ) + ), + "reason" to "To legally be able to open your account", + "essential" to true + ), + "user_info" to mapOf( + "name" to mapOf( + "essential" to true, + "reason" to "Show your name to other users" + ), + "country" to null + ) + ) + )*/ + + val claim = ClaimsRequestParams() + .addVerifiable( + "email", + VerifiableParams( + "We need to be able to email you", + true + ) + .addIssuer("did:web:uport.claims", "https://uport.claims/email") + .addIssuer("did:web:sobol.io", "https://sobol.io/verify") + ) + .addVerifiable( + "nationalIdentity", + VerifiableParams( + "To legally be able to open your account" + ) + .addIssuer("did:web:idverifier.claims", "https://idverifier.example") + ) + .addUserInfo("name", UserInfoParams("Show your name to other users", true)) + .addUserInfo("country", UserInfoParams("Show your country to other users", true)) + .build() + // create the request JWT val cred = Credentials(issuerDID, signer) val params = SelectiveDisclosureRequestParams( - requested = listOf("name"), - callbackUrl = "https://uport-project.github.io/uport-android-sdk/callbacks" + requested = listOf("name"), + callbackUrl = "https://uport-project.github.io/uport-android-sdk/callbacks", + claims = claim ) request_details.text = """