From d93df10f55c0b7e022cae960e86b4a6dd81705c1 Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Mon, 12 Feb 2024 21:07:15 +0000 Subject: [PATCH 01/11] feat: add abstract, motivation and header specification of arc-0080 --- ARCs/arc-0080.md | 127 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 ARCs/arc-0080.md diff --git a/ARCs/arc-0080.md b/ARCs/arc-0080.md new file mode 100644 index 000000000..c2b5957ce --- /dev/null +++ b/ARCs/arc-0080.md @@ -0,0 +1,127 @@ +--- +arc: 80 +title: Authentication with JSON Web Token (JWT) +description: A specification that outlines authentication with JSON Web Tokens (JWT). +author: Kieran O'Neill (@kieranroneill) +status: Draft +type: Standards Track +category: Interface +created: 2024-02-12 +--- + +# Authentication with JSON Web Token (JWT) + +## Abstract + +JSON Web Tokens (JWTs) were proposed in the specification [RFC 7519][rfc-7519] and are a popular format that represent a claim that can be transferred between two parties. + +This proposal aims to build upon the JWT standard, but with a focus on using JWTs within the context of an Algorand account; a public/private key pair using Ed25519 elliptic-curve signatures. + +## Motivation + +Authentication is a wide-ranging subject, and it's implementation can be done in many different ways. This proposal does not aim to enforce a defacto authentication method for Algorand, but merely outlines a popular authentication method: JSON Web Token (JWT), within the context of an Algorand account. + +The intention of this proposal is to live alongside other authentication methods. + +## Specification + +The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in RFC-2119. + +> Comments like this are non-normative. + +### Definitions + +This section is non-normative. + +* Claim + - A piece of information asserted about a subject, represented as a name/value pair. +* JSON Web Algorithms (JWA) + - A registered list of cryptographic algorithms by the [IANA][iana-jwa-list]; it represents the allowed values of the `"alg"` parameter of a JSON Web Token (JWT)'s header. +* JSON Web Key (JWK) + - A JSON object that represents the structure of the cryptographic mechanism using the JSON Web Signature (JWS). +* JSON Web Signature (JWS) + - Represents a content signature signed with a cryptographic mechanism. +* JSON Web Token (JWT) + - A JSON object, with a set of claims, encoded in a JSON Web Signature (JWS). + +### Overview + +JSON Web Token (JWT) is an open standard ([RFC 7519][rfc-7519]) that allows a compact, URL-safe means of representing claims that can be transferred between two parties. + +A JWT consists of three parts separated by dots (`.`): + +* Header - contains information about the type of token and the signing algorithm used. +* Payload - contains the claims, which are statements about an entity (typically, the user) and additional data. +* Signature - is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. + +Therefore, a JWT will look like the following: +``` +header.payload.signature +``` + +> ⚠️ **NOTE**: Each part of the JWT **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding. + +### Header + +An Algorand address is essentially a transformed public key of a key pair that was created using an EdDSA signature with the Curve25519; or Ed25519 for short. + +In order for EdDSA keys to be encoded as a JWK, the standard [RFC 8037][rfc-8037] introduced a new key type: "OKP" (Octet Key Pair), that outlines use of public key algorithms, namely Ed25519 ([RFC 8032][rfc-8032]), with a JOSE header. + +When constructing the header for a JWT for Algorand accounts, the header must conform to the following: +```json +{ + "alg": "EdDSA", + "crv": "Ed25519", + "kty": "OKP", + "typ": "JWT", + "x": "VEji5WHLRFmFuONhBmMnflYhHj7xVZ2OD748FF7r5hE=" +} +``` +where: +* `alg`: + - **REQUIRED** as per [RFC 7515 section 4.1.1.][rfc-7515-section-411] that defines the cryptographic algorithm used to create an JWS. + - **MUST** be `EdDSA` to indicate an Edwards-curve Digital Signature Algorithm (EdDSA) as used by Algorand accounts. +* `crv`: + - **REQUIRED** as per [RFC 8037 section 2][rfc-8037-section-2] + - **MUST** be `Ed25519` to indicate the name of the curve, which is Curve25519 for Algorand accounts. +* `kty`: + - **OPTIONAL** the `"alg"` parameter is sufficient to infer the JWK. + - **MUST** be `OKP` as specified in [RFC 8037][rfc-8037] for EdDSA keys. +* `typ`: + - **OPTIONAL** as applications usually know this is a JWT. + - **RECOMMENDED** to be `JWT` to indicate this is a JWT. +* `x`: + - **REQUIRED** as per [RFC 8037 section 2][rfc-8037-section-2] and to verify the signature has been signed with the correct private key of an Algorand account. + - **MUST** be the public key of an Algorand account, encoded using the base64URL ([RFC 4648][rfc-4648-section-5]) encoding. + +### Payload + + + +### Signature + + + +## Rationale + + + +## Reference Implementation + + + +## Security Considerations + +None. + +## Copyright +Copyright and related rights waived via CCO. + + +[iana-jwa-list]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms +[rfc-4648-section-5]: https://datatracker.ietf.org/doc/html/rfc4648#section-5 +[rfc-7515-section-411]: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1 +[rfc-7519]: https://datatracker.ietf.org/doc/html/rfc7519 +[rfc-8032]: https://datatracker.ietf.org/doc/html/rfc8032 +[rfc-8037]: https://datatracker.ietf.org/doc/html/rfc8037 +[rfc-8037-section-2]: https://datatracker.ietf.org/doc/html/rfc8037#section-2 From f9f431def5bccf9a4a075ab54085dd62416426f5 Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Mon, 12 Feb 2024 22:36:07 +0000 Subject: [PATCH 02/11] feat: add payload --- ARCs/arc-0080.md | 86 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/ARCs/arc-0080.md b/ARCs/arc-0080.md index c2b5957ce..360897509 100644 --- a/ARCs/arc-0080.md +++ b/ARCs/arc-0080.md @@ -17,9 +17,11 @@ JSON Web Tokens (JWTs) were proposed in the specification [RFC 7519][rfc-7519] a This proposal aims to build upon the JWT standard, but with a focus on using JWTs within the context of an Algorand account; a public/private key pair using Ed25519 elliptic-curve signatures. +The primary purpose of this proposal is to identify the subject of a JWT; the Algorand account, and verify it. + ## Motivation -Authentication is a wide-ranging subject, and it's implementation can be done in many different ways. This proposal does not aim to enforce a defacto authentication method for Algorand, but merely outlines a popular authentication method: JSON Web Token (JWT), within the context of an Algorand account. +Authentication is a wide-ranging subject, and it's implementation can be done in many different ways. This proposal does not aim to enforce a defacto authentication method for Algorand, but to merely outline a possible authentication method: JSON Web Token (JWT) within the context of an Algorand account. The intention of this proposal is to live alongside other authentication methods. @@ -34,7 +36,11 @@ The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL This section is non-normative. * Claim - - A piece of information asserted about a subject, represented as a name/value pair. + - A piece of information asserted about a subject, represented as a key/value pair. +* Claim Name + - The "key" part of the claim (key/value pair). It **MUST** be a string. +* Claim Value + - The "value" part of the claim (key/value pair). It **MAY** be any JSON value. * JSON Web Algorithms (JWA) - A registered list of cryptographic algorithms by the [IANA][iana-jwa-list]; it represents the allowed values of the `"alg"` parameter of a JSON Web Token (JWT)'s header. * JSON Web Key (JWK) @@ -50,9 +56,9 @@ JSON Web Token (JWT) is an open standard ([RFC 7519][rfc-7519]) that allows a co A JWT consists of three parts separated by dots (`.`): -* Header - contains information about the type of token and the signing algorithm used. -* Payload - contains the claims, which are statements about an entity (typically, the user) and additional data. -* Signature - is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. +* **Header** - contains information about the type of token and the signing algorithm used. +* **Payload** - contains the claims, which are statements about an entity (typically, the user) and additional data. +* **Signature** - is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. Therefore, a JWT will look like the following: ``` @@ -65,7 +71,7 @@ header.payload.signature An Algorand address is essentially a transformed public key of a key pair that was created using an EdDSA signature with the Curve25519; or Ed25519 for short. -In order for EdDSA keys to be encoded as a JWK, the standard [RFC 8037][rfc-8037] introduced a new key type: "OKP" (Octet Key Pair), that outlines use of public key algorithms, namely Ed25519 ([RFC 8032][rfc-8032]), with a JOSE header. +In order for EdDSA keys to be encoded as a JWK, the standard [RFC 8037][rfc-8037] introduced a new key type: "OKP" (Octet Key Pair), which outlines use of public key algorithms, namely Ed25519 ([RFC 8032][rfc-8032]), with a JOSE header. When constructing the header for a JWT for Algorand accounts, the header must conform to the following: ```json @@ -96,19 +102,72 @@ where: ### Payload +The payload contains the claims of the JWT. A claim is a JSON object of key/value pairs that can provide specific details about the entity (the owner of the Algorand account) or the intention of the entity. While the structure of the claim is not enforced, each claim name **MUST** be unique and the recipient **MUST** reject the JWT if there are duplicate claim names. + +A claim name can fall under one of three types: +* **Registered** - are claim names that have been registered in the IANA "JSON Web Token Claims" registry and while each is not mandatory they are **RECOMMENDED**. +* **Public** - are claim names that are a public name; a value that contains a collision-resistant name. +* **Private** - are custom claim names that are agreed by all parties and are neither _registered_ nor _public_. + +```json +{ + "aud": "api.awesome.com", + "exp": 1707782400, + "iat": 1707696000, + "iss": "dapp.awesome.com", + "jti": "22080a89-a283-48e7-96c5-87f17ce7a850", + "nbf": 1707739200, + "sub": "KREOFZLBZNCFTBNY4NQQMYZHPZLCCHR66FKZ3DQPXY6BIXXL4YIS232YAU" +} +``` +where: +* `aud`: + - **OPTIONAL** as per [RFC 7519 section 4.1.3.][rfc-7519-section-413] this claim identifies the intended recipient(s) of the JWT. + - **MUST** be rejected if the recipient of the JWT cannot identify itself with the value. + - **MAY** be an array of string or URI ([RFC 3986][rfc-3986]), if there are multiple intended recipients. + - **MUST** be a string or URI ([RFC 3986][rfc-3986]), if the intended recipient is one. +* `exp`: + - **OPTIONAL** as per [RFC 7519 section 4.1.4.][rfc-7519-section-414] this claim identifies the date/time this JWT expires. + - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. + - **MUST** be rejected if the expiration time has passed as compared to the current date/time. +* `iat`: + - **OPTIONAL** as per [RFC 7519 section 4.1.6.][rfc-7519-section-416] this claim identifies the date/time this JWT was issued. + - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. +* `iss`: + - **OPTIONAL** as per [RFC 7519 section 4.1.1.][rfc-7519-section-411] this claim identifies the entity that issued the JWT. This can be a dApp or a wallet. + - **MUST** be a string or URI ([RFC 3986][rfc-3986]). +* `jti`: + - **OPTIONAL** as per [RFC 7519 section 4.1.7.][rfc-7519-section-417] this claim identifies the recipient of the JWT. This claim is used to prevent the same JWT being replayed. + - **MUST** be a value that ensures a negligible probability that the same value will appear on another JWT. + - **RECOMMENDED** be a UUIDv4 ([RFC 4122][rfc-4122]) compliant string. +* `nbf`: + - **OPTIONAL** as per [RFC 7519 section 4.1.5.][rfc-7519-section-415] this claim identifies the date/time after which this JWT becomes valid. + - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. + - **MUST** be rejected if the not before time is after the current date/time. +* `sub`: + - **OPTIONAL** as per [RFC 7519 section 4.1.2.][rfc-7519-section-412] this claim identifies the subject of the JWT. + - **RECOMMENDED** be the public key of the key pair used in the JWS, with a 4 byte checksum appended and encoded using base32 ([RFC 4648][rfc-4648-section-6]) encoding, as defined in ([keys and addresses][algorand-keys-and-addresses]); the Algorand address. ### Signature -## Rationale +### Verification + + +### Authentication + + + +## Rationale + ## Reference Implementation - + ## Security Considerations @@ -119,9 +178,20 @@ Copyright and related rights waived via RFC-2119. > Comments like this are non-normative. +[Back to top ^][title] + ### Definitions This section is non-normative. @@ -50,6 +56,8 @@ This section is non-normative. * JSON Web Token (JWT) - A JSON object, with a set of claims, encoded in a JSON Web Signature (JWS). +[Back to top ^][title] + ### Overview JSON Web Token (JWT) is an open standard ([RFC 7519][rfc-7519]) that allows a compact, URL-safe means of representing claims that can be transferred between two parties. @@ -67,6 +75,8 @@ header.payload.signature > ⚠️ **NOTE**: Each part of the JWT **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding. +[Back to top ^][title] + ### Header An Algorand address is essentially a transformed public key of a key pair that was created using an EdDSA signature with the Curve25519; or Ed25519 for short. @@ -100,6 +110,8 @@ where: - **REQUIRED** as per [RFC 8037 section 2][rfc-8037-section-2] and to verify the signature has been signed with the correct private key of an Algorand account. - **MUST** be the public key of an Algorand account, encoded using the base64URL ([RFC 4648][rfc-4648-section-5]) encoding. +[Back to top ^][title] + ### Payload The payload contains the claims of the JWT. A claim is a JSON object of key/value pairs that can provide specific details about the entity (the owner of the Algorand account) or the intention of the entity. While the structure of the claim is not enforced, each claim name **MUST** be unique and the recipient **MUST** reject the JWT if there are duplicate claim names. @@ -149,33 +161,47 @@ where: - **OPTIONAL** as per [RFC 7519 section 4.1.2.][rfc-7519-section-412] this claim identifies the subject of the JWT. - **RECOMMENDED** be the public key of the key pair used in the JWS, with a 4 byte checksum appended and encoded using base32 ([RFC 4648][rfc-4648-section-6]) encoding, as defined in ([keys and addresses][algorand-keys-and-addresses]); the Algorand address. -### Signature +[Back to top ^][title] +### Signature +[Back to top ^][title] ### Verification +[Back to top ^][title] + ### Authentication +[Back to top ^][title] + ## Rationale +[Back to top ^][title] + ## Reference Implementation +[Back to top ^][title] + ## Security Considerations None. +[Back to top ^][title] + ## Copyright Copyright and related rights waived via CCO. +[Back to top ^][title] + [iana-jwa-list]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms [algorand-keys-and-addresses]: https://developer.algorand.org/docs/get-details/accounts/#keys-and-addresses @@ -195,3 +221,4 @@ Copyright and related rights waived via [Back to top ^][title] @@ -124,51 +151,176 @@ A claim name can fall under one of three types: ```json { - "aud": "api.awesome.com", - "exp": 1707782400, - "iat": 1707696000, - "iss": "dapp.awesome.com", - "jti": "22080a89-a283-48e7-96c5-87f17ce7a850", - "nbf": 1707739200, - "sub": "KREOFZLBZNCFTBNY4NQQMYZHPZLCCHR66FKZ3DQPXY6BIXXL4YIS232YAU" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "JSON Web Token (JWT) Payload", + "type": "object", + "properties": { + "aud": { + "description": "Defines the intended recipient(s) of the JWT", + "anyOf": [ + { + "type": "string" + }, + { + "format": "uri", + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "uri", + "type": "string" + } + ] + } + } + ] + }, + "exp": { + "description": "Defines the date/time the JWT expires", + "type": "number" + }, + "iat": { + "description": "Defines the date/time this JWT the issued", + "type": "number" + }, + "iss": { + "description": "Defines the entity that issued the JWT", + "oneOf": [ + { + "type": "string" + }, + { + "format": "uri", + "type": "string" + } + ] + }, + "jti": { + "description": "A unique identifier for the JWT that is used to prevent the same JWT being replayed", + "type": "string" + }, + "nbf": { + "description": "Defines the date/time after which the JWT becomes valid", + "type": "number" + }, + "sub": { + "description": "Defines the subject of the JWT", + "type": "string" + } + } } ``` + where: * `aud`: - - **OPTIONAL** as per [RFC 7519 section 4.1.3.][rfc-7519-section-413] this claim identifies the intended recipient(s) of the JWT. + - **OPTIONAL** as per [RFC 7519 section 4.1.3.][rfc-7519-section-413] - **MUST** be rejected if the recipient of the JWT cannot identify itself with the value. - **MAY** be an array of string or URI ([RFC 3986][rfc-3986]), if there are multiple intended recipients. - **MUST** be a string or URI ([RFC 3986][rfc-3986]), if the intended recipient is one. * `exp`: - - **OPTIONAL** as per [RFC 7519 section 4.1.4.][rfc-7519-section-414] this claim identifies the date/time this JWT expires. + - **OPTIONAL** as per [RFC 7519 section 4.1.4.][rfc-7519-section-414] - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. - - **MUST** be rejected if the expiration time has passed as compared to the current date/time. + - **MUST** be rejected if the current date/time is after the expiration time. * `iat`: - - **OPTIONAL** as per [RFC 7519 section 4.1.6.][rfc-7519-section-416] this claim identifies the date/time this JWT was issued. + - **OPTIONAL** as per [RFC 7519 section 4.1.6.][rfc-7519-section-416] - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. * `iss`: - - **OPTIONAL** as per [RFC 7519 section 4.1.1.][rfc-7519-section-411] this claim identifies the entity that issued the JWT. This can be a dApp or a wallet. + - **OPTIONAL** as per [RFC 7519 section 4.1.1.][rfc-7519-section-411] - **MUST** be a string or URI ([RFC 3986][rfc-3986]). + - **RECOMMENDED** be the dApp or wallet. * `jti`: - - **OPTIONAL** as per [RFC 7519 section 4.1.7.][rfc-7519-section-417] this claim identifies the recipient of the JWT. This claim is used to prevent the same JWT being replayed. + - **OPTIONAL** as per [RFC 7519 section 4.1.7.][rfc-7519-section-417] - **MUST** be a value that ensures a negligible probability that the same value will appear on another JWT. - **RECOMMENDED** be a UUIDv4 ([RFC 4122][rfc-4122]) compliant string. * `nbf`: - - **OPTIONAL** as per [RFC 7519 section 4.1.5.][rfc-7519-section-415] this claim identifies the date/time after which this JWT becomes valid. + - **OPTIONAL** as per [RFC 7519 section 4.1.5.][rfc-7519-section-415] - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. - - **MUST** be rejected if the not before time is after the current date/time. + - **MUST** be rejected if the current date/time is before the "not before" time. * `sub`: - - **OPTIONAL** as per [RFC 7519 section 4.1.2.][rfc-7519-section-412] this claim identifies the subject of the JWT. - - **RECOMMENDED** be the public key of the key pair used in the JWS, with a 4 byte checksum appended and encoded using base32 ([RFC 4648][rfc-4648-section-6]) encoding, as defined in ([keys and addresses][algorand-keys-and-addresses]); the Algorand address. + - **OPTIONAL** as per [RFC 7519 section 4.1.2.][rfc-7519-section-412] + - **RECOMMENDED** be the public key of the key pair used in the JWS, with a 4 byte checksum appended and encoded using base32 ([RFC 4648][rfc-4648-section-6]) encoding; the Algorand address, as defined in ([keys and addresses][algorand-keys-and-addresses]). [Back to top ^][title] ### Signature +The signature is used to verify the message isn't changed along the way, and is signed with the private key of the Algorand account. + +The signature is made up of the header and the payload, where each part **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding, concatenated with a dot (`.`), and then the resulting string is signed using the private key of the Algorand account. Finally, the signature's bytes **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding. + +``` +ed25529Sign( + base64UrlEncode(header) + "." + base64UrlEncode(payload), + privateKey +) +``` + +For example, given a header: + +```json +{ + "alg": "EdDSA", + "crv": "Ed25519", + "kty": "OKP", + "typ": "JWT", + "x": "FrMUY1-U6zLa5xz3r0RUmILVwqyIY30HX4JWEXTN2grk9Djs" +} +``` + +And a payload: + +```json +{ + "aud": "https://api.awesome.com", + "exp": 1707782400, + "iat": 1707696000, + "iss": "https://dapp.awesome.com", + "jti": "22080a89-a283-48e7-96c5-87f17ce7a850", + "nbf": 1707739200, + "sub": "C2ZRIY27STVTFWXHDT326RCUTCBNLQVMRBRX2B27QJLBC5GN3IFOJ5BY5Q" +} +``` + +Encode each part using base64URL ([RFC 4648][rfc-4648-section-5]) encoding and concatenate with a dot (`.`) to produce: + +``` +eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ0eXAiOiJKV1QiLCJ4IjoiRnJNVVkxLVU2ekxhNXh6M3IwUlVtSUxWd3F5SVkzMEhYNEpXRVhUTjJncms5RGpzIn0. +eyJhdWQiOiJodHRwczovL2FwaS5hd2Vzb21lLmNvbSIsImV4cCI6MTcwNzc4MjQwMCwiaWF0IjoxNzA3Njk2MDAwLCJpc3MiOiJodHRwczovL2RhcHAuYXdlc29tZS5jb20iLCJqdGkiOiIyMjA4MGE4OS1hMjgzLTQ4ZTctOTZjNS04N2YxN2NlN2E4NTAiLCJuYmYiOjE3MDc3MzkyMDAsInN1YiI6IkMyWlJJWTI3U1RWVEZXWEhEVDMyNlJDVVRDQk5MUVZNUkJSWDJCMjdRSkxCQzVHTjNJRk9KNUJZNVEifQ +``` + +> ⚠️ **NOTE**: line breaks have been used in the above example. + +Finally, the message can be signed using the private key, encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding, to produce the following signature: + +``` +wRUxp_9qLAutcXpzVaxKZpe3foQfQU2CDsIQZLXvVadhpDFN52ZYDHq3hk4C7bw65DetjRiaCCsPzj86I-QjDg== +``` + +[Back to top ^][title] + +### A JSON Web Token + +Once the header, payload and signature have been created, the complete token is simply a concatenation of the three parts using a dot (`.`) as a delimiter: + +``` +eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ0eXAiOiJKV1QiLCJ4IjoiRnJNVVkxLVU2ekxhNXh6M3IwUlVtSUxWd3F5SVkzMEhYNEpXRVhUTjJncms5RGpzIn0. +eyJhdWQiOiJodHRwczovL2FwaS5hd2Vzb21lLmNvbSIsImV4cCI6MTcwNzc4MjQwMCwiaWF0IjoxNzA3Njk2MDAwLCJpc3MiOiJodHRwczovL2RhcHAuYXdlc29tZS5jb20iLCJqdGkiOiIyMjA4MGE4OS1hMjgzLTQ4ZTctOTZjNS04N2YxN2NlN2E4NTAiLCJuYmYiOjE3MDc3MzkyMDAsInN1YiI6IkMyWlJJWTI3U1RWVEZXWEhEVDMyNlJDVVRDQk5MUVZNUkJSWDJCMjdRSkxCQzVHTjNJRk9KNUJZNVEifQ. +wRUxp_9qLAutcXpzVaxKZpe3foQfQU2CDsIQZLXvVadhpDFN52ZYDHq3hk4C7bw65DetjRiaCCsPzj86I-QjDg== +``` + +> ⚠️ **NOTE**: line breaks have been used in the above example. + [Back to top ^][title] ### Verification +To verify the token, you can take the signature, [Back to top ^][title] @@ -187,7 +339,76 @@ where: ## Reference Implementation - +> 🚨 **WARNING**: The below examples use third party libraries and while they serve merely as examples, you should choose your own cryptographic implementation based an implementation you trust and that has been well audited. + +[Back to top ^][title] + +### 1. A TypeScript Implementation Using TweetNaCl.js + +The following examples use the popular [TweetNaCl.js][tweetnacl-js] library for cryptographic signing and verification. + +> ⚠️ **NOTE**: At the time of writing this ARC [TweetNaCl.js][tweetnacl-js] was last audited between January-February 2017, you can read the full audit report [here](https://cure53.de/tweetnacl.pdf). + +For creating and signing a JWT: + +```typescript +import { decodeAddress, mnemonicToSecretKey } from 'algosdk'; +import { sign } from 'tweetnacl'; + +const { addr, sk } = mnemonicToSecretKey('host degree host...'); // get the address and private key from the 25-word mnemonic seed phrase +const encodedPublicKey: string = Buffer.from(decodeAddress(addr).publicKey).toString('base64url'); +const header: string = JSON.stringify({ + alg: 'EdDSA', + crv: 'Ed25519', + kty: 'OKP', + typ: 'JWT', + x: encodedPublicKey, // FrMUY1-U6zLa5xz3r0RUmILVwqyIY30HX4JWEXTN2grk9Djs +}); +const payload: string = JSON.stringify({ + aud: 'https://api.awesome.com', + exp: 1707782400, + iat: 1707696000, + iss: 'https://dapp.awesome.com', + jti: '22080a89-a283-48e7-96c5-87f17ce7a850', + nbf: 1707739200, + sub: addr, // C2ZRIY27STVTFWXHDT326RCUTCBNLQVMRBRX2B27QJLBC5GN3IFOJ5BY5Q +}); +const encodedHeader: string = Buffer.from(header).toString('base64url'); +const encodedPayload: string = Buffer.from(payload).toString('base64url'); +const signature: Uint8Array = sign.detached( + new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`), + sk, // sign with the algorand account's private key +); +const encodedSignature: string = Buffer.from(signature).toString('base64url'); + +console.log(`json web token: ${encodedHeader}.${encodedPayload}.${encodedSignature}`); +/* +json web token: eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ0eXAiOiJKV1QiLCJ4IjoiRnJNVVkxLVU2ekxhNXh6M3IwUlVtSUxWd3F5SVkzMEhYNEpXRVhUTjJncms5RGpzIn0.eyJhdWQiOiJodHRwczovL2FwaS5hd2Vzb21lLmNvbSIsImV4cCI6MTcwNzc4MjQwMCwiaWF0IjoxNzA3Njk2MDAwLCJpc3MiOiJodHRwczovL2RhcHAuYXdlc29tZS5jb20iLCJqdGkiOiIyMjA4MGE4OS1hMjgzLTQ4ZTctOTZjNS04N2YxN2NlN2E4NTAiLCJuYmYiOjE3MDc3MzkyMDAsInN1YiI6IkMyWlJJWTI3U1RWVEZXWEhEVDMyNlJDVVRDQk5MUVZNUkJSWDJCMjdRSkxCQzVHTjNJRk9KNUJZNVEifQ.wRUxp_9qLAutcXpzVaxKZpe3foQfQU2CDsIQZLXvVadhpDFN52ZYDHq3hk4C7bw65DetjRiaCCsPzj86I-QjDg== + */ +``` + +To verify the JWT: + +```typescript +import { decodeAddress, mnemonicToSecretKey } from 'algosdk'; +import { sign } from 'tweetnacl'; + +const [encodedHeader, encodedPayload, encodedSignature] = jwt.split('.'); +const decodedHeader: Uint8Array = Buffer.from(encodedHeader, 'base64url'); +const decodedSignature: Uint8Array = Buffer.from(encodedSignature, 'base64url'); +const { x } = JSON.parse(decodedHeader.toString()); // get the "x" parameter; the public key, from the header +const decodedPublicKey: Uint8Array = Buffer.from(x, 'base64url'); +const isVerified: boolean = sign.detached.verify( + new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`), // re-create the signed message + decodedSignature, + decodedPublicKey, +); + +console.log('is verified: ', isVerified); +/* +is verified: true + */ +``` [Back to top ^][title] @@ -222,3 +443,4 @@ Copyright and related rights waived via [Back to top ^][title] + ## Security Considerations None. @@ -426,6 +550,7 @@ Copyright and related rights waived via [Back to top ^][title] @@ -555,6 +614,7 @@ Copyright and related rights waived via [Back to top ^][title] @@ -81,9 +83,9 @@ header.payload.signature An Algorand address is essentially a transformed public key of a key pair that was created using an EdDSA signature with the Curve25519; or Ed25519 for short. -In order for EdDSA keys to be encoded as a JWK, the standard [RFC 8037][rfc-8037] introduced a new key type: "OKP" (Octet Key Pair), which outlines use of public key algorithms, namely Ed25519 ([RFC 8032][rfc-8032]), with a JOSE header. +In order for EdDSA keys to be encoded as a JWK, the standard [RFC 8037][rfc-8037] introduced a new key type: "OKP" (Octet Key Pair), which describes the use of public key algorithms, namely Ed25519 ([RFC 8032][rfc-8032]), with a JOSE header. -When constructing the header for a JWT for Algorand accounts, the header must conform to the following: +When constructing the header of a JWT for Algorand accounts, the header must conform to the following: ```json { @@ -252,7 +254,7 @@ where: The signature is used to verify the message isn't changed along the way, and is signed with the private key of the Algorand account. -As defined in [RFC 7515 section 3][rfc-7515-section-3], the signature is a JSOE header, whose members are the union of the header and payload, where each part **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding, concatenated with a dot (`.`), and then the resulting string is signed using the private key of the Algorand account. +As defined in [RFC 7515 section 3][rfc-7515-section-3], the signature is a JSOE header, whose members are the union of the header and the payload, where each part **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding, concatenated with a dot (`.`), and then the resulting string is signed using the private key of the Algorand account. Finally, the signature's bytes **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding. @@ -320,22 +322,9 @@ wRUxp_9qLAutcXpzVaxKZpe3foQfQU2CDsIQZLXvVadhpDFN52ZYDHq3hk4C7bw65DetjRiaCCsPzj86 [Back to top ^][title] -### Verification - -To verify the token, you can take the signature, - - -[Back to top ^][title] - -### Authentication - - - -[Back to top ^][title] - ## Rationale - +The intention of this proposal is to live alongside other authentication methods, it does not aim to enforce a defacto authentication method for Algorand, but to merely outline a possible authentication method: JSON Web Token (JWT) within the context of an Algorand account. [Back to top ^][title] @@ -347,9 +336,9 @@ To verify the token, you can take the signature, ### 1. A TypeScript Implementation Using TweetNaCl.js -The following example use the popular [TweetNaCl.js][tweetnacl-js] library for cryptographic signing and verification. +The following example uses the popular [TweetNaCl.js][tweetnacl-js] library for cryptographic signing and verification. -> ⚠️ **NOTE**: At the time of writing this proposal [TweetNaCl.js][tweetnacl-js] was last audited between January-February 2017, you can read the full audit report [here](https://cure53.de/tweetnacl.pdf). +> ⚠️ **NOTE**: At the time of writing, [TweetNaCl.js][tweetnacl-js] was last audited between January-February 2017. The full audit report can be read [here](https://cure53.de/tweetnacl.pdf). For creating and signing a JWT: @@ -627,5 +616,6 @@ Copyright and related rights waived via RFC 7519 and This proposal aims to outline the JWT standard, but focus on using JWTs within the context of an Algorand account; a public/private key pair using Ed25519 elliptic-curve signatures. The primary purpose of this proposal is to identify the subject of a JWT; the Algorand account, and verify it. -[Back to top ^][title] - ## Motivation Authentication is a wide-ranging subject and can be done in many different ways. JWTs have proven to be a hugely popular standard when it comes to authentication and, as Algorand accounts, at their core, are public/private key pairs using Ed25519 elliptic-curve signatures, they can easily be used to sign JWTs. -Furthermore, JWTs that are signed using an Algorand account's private key, can be used by clients (such as dApps) to prove the identity of a user and can open up clients to allow limited access to their services using methods such as "scope" (as is suggested in [RFC 8693 section 4.2][rfc-8693-section-42]). +Furthermore, JWTs that are signed using an Algorand account's private key, can be used by clients (such as dApps) to prove the identity of a user and can open up clients to allow limited access to their services using methods such as "scope" (as is suggested in RFC 8693 section 4.2). This proposal serves as the basis for constructing a JWT. -[Back to top ^][title] - ## Specification The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in RFC-2119. > Comments like this are non-normative. -[Back to top ^][title] - ### Definitions This section is non-normative. * Claim - - A piece of information asserted about a subject, represented as a key/value pair. + * A piece of information asserted about a subject, represented as a key/value pair. * Claim Name - - The "key" part of the claim (key/value pair). It **MUST** be a string. + * The "key" part of the claim (key/value pair). It **MUST** be a string. * Claim Value - - The "value" part of the claim (key/value pair). It **MAY** be any JSON value. + * The "value" part of the claim (key/value pair). It **MAY** be any JSON value. * JSON Web Algorithms (JWA) - - A registered list of cryptographic algorithms by the [IANA][iana-jwa-list]; it represents the allowed values of the `"alg"` parameter of a JSON Web Token (JWT)'s header. + * A registered list of cryptographic algorithms by the IANA; it represents the allowed values of the `"alg"` parameter of a JSON Web Token (JWT)'s header. * JSON Web Key (JWK) - - A JSON object that represents the structure of the cryptographic mechanism using the JSON Web Signature (JWS). + * A JSON object that represents the structure of the cryptographic mechanism using the JSON Web Signature (JWS). * JSON Web Signature (JWS) - - Represents a content signature signed with a cryptographic mechanism. + * Represents a content signature signed with a cryptographic mechanism. * JSON Web Token (JWT) - - A JSON object, with a set of claims, encoded in a JSON Web Signature (JWS). - -[Back to top ^][title] + * A JSON object, with a set of claims, encoded in a JSON Web Signature (JWS). ### Overview -JSON Web Token (JWT) is an open standard ([RFC 7519][rfc-7519]) that allows a compact, URL-safe means of representing claims that can be transferred between two parties. +JSON Web Token (JWT) is an open standard (RFC 7519) that allows a compact, URL-safe means of representing claims that can be transferred between two parties. -A JWT consists of three parts separated by dots (`.`): +A JWT consists of three parts separated by dots (`.`): * **Header** - contains information about the type of token and the signing algorithm used. * **Payload** - contains the claims, which are statements about an entity (typically, the user) and additional data. * **Signature** - is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. Therefore, a JWT will look like the following: + ``` header.payload.signature ``` -> ⚠️ **NOTE**: Each part of the JWT **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding. - -[Back to top ^][title] +> ⚠️ **NOTE**: Each part of the JWT **MUST** be encoded using base64URL (RFC 4648 Section 5) encoding. ### Header An Algorand address is essentially a transformed public key of a key pair that was created using an EdDSA signature with the Curve25519; or Ed25519 for short. -In order for EdDSA keys to be encoded as a JWK, the standard [RFC 8037][rfc-8037] introduced a new key type: "OKP" (Octet Key Pair), which describes the use of public key algorithms, namely Ed25519 ([RFC 8032][rfc-8032]), with a JOSE header. +In order for EdDSA keys to be encoded as a JWK, the standard RFC 8037 introduced a new key type: "OKP" (Octet Key Pair), which describes the use of public key algorithms, namely Ed25519 (RFC 8032), with a JOSE header. When constructing the header of a JWT for Algorand accounts, the header must conform to the following: @@ -123,23 +114,22 @@ When constructing the header of a JWT for Algorand accounts, the header must con ``` where: + * `alg`: - - **REQUIRED** as per [RFC 7515 section 4.1.1.][rfc-7515-section-411] - - **MUST** be `EdDSA` to indicate an Edwards-curve Digital Signature Algorithm (EdDSA) as used by Algorand accounts. + * **REQUIRED** as per RFC 7515 section 4.1.1. + * **MUST** be `EdDSA` to indicate an Edwards-curve Digital Signature Algorithm (EdDSA) as used by Algorand accounts. * `crv`: - - **REQUIRED** as per [RFC 8037 section 2][rfc-8037-section-2]. - - **MUST** be `Ed25519` to indicate the name of the curve, which is Curve25519, for Algorand accounts. + * **REQUIRED** as per RFC 8037 section 2. + * **MUST** be `Ed25519` to indicate the name of the curve, which is Curve25519, for Algorand accounts. * `kty`: - - **OPTIONAL** the `"alg"` parameter is sufficient to infer the JWK. - - **MUST** be `OKP` as specified in [RFC 8037][rfc-8037] for EdDSA keys. + * **OPTIONAL** the `"alg"` parameter is sufficient to infer the JWK. + * **MUST** be `OKP` as specified in RFC 8037 for EdDSA keys. * `typ`: - - **OPTIONAL** as applications usually know this is a JWT. - - **RECOMMENDED** to be `JWT` to indicate this is a JWT. + * **OPTIONAL** as applications usually know this is a JWT. + * **RECOMMENDED** to be `JWT` to indicate this is a JWT. * `x`: - - **REQUIRED** as per [RFC 8037 section 2][rfc-8037-section-2]. - - **MUST** be the public key of an Algorand account, encoded using the base64URL ([RFC 4648][rfc-4648-section-5]) encoding. - -[Back to top ^][title] + * **REQUIRED** as per RFC 8037 section 2. + * **MUST** be the public key of an Algorand account, encoded using the base64URL (RFC 4648) encoding. ### Payload @@ -220,43 +210,42 @@ A claim name can fall under one of three types: ``` where: + * `aud`: - - **OPTIONAL** as per [RFC 7519 section 4.1.3.][rfc-7519-section-413] - - **MUST** be rejected if the recipient of the JWT cannot identify itself with the value. - - **MAY** be an array of string or URI ([RFC 3986][rfc-3986]), if there are multiple intended recipients. - - **MUST** be a string or URI ([RFC 3986][rfc-3986]), if the intended recipient is one. + * **OPTIONAL** as per RFC 7519 section 4.1.3 + * **MUST** be rejected if the recipient of the JWT cannot identify itself with the value. + * **MAY** be an array of string or URI (RFC 3986), if there are multiple intended recipients. + * **MUST** be a string or URI (RFC 3986), if the intended recipient is one. * `exp`: - - **OPTIONAL** as per [RFC 7519 section 4.1.4.][rfc-7519-section-414] - - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. - - **MUST** be rejected if the current date/time is after the expiration time. + * **OPTIONAL** as per RFC 7519 section 4.1.4 + * **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. + * **MUST** be rejected if the current date/time is after the expiration time. * `iat`: - - **OPTIONAL** as per [RFC 7519 section 4.1.6.][rfc-7519-section-416] - - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. + * **OPTIONAL** as per RFC 7519 section 4.1.6 + * **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. * `iss`: - - **OPTIONAL** as per [RFC 7519 section 4.1.1.][rfc-7519-section-411] - - **MUST** be a string or URI ([RFC 3986][rfc-3986]). - - **RECOMMENDED** be the dApp or wallet. + * **OPTIONAL** as per RFC 7519 section 4.1.1 + * **MUST** be a string or URI (RFC 3986). + * **RECOMMENDED** be the dApp or wallet. * `jti`: - - **OPTIONAL** as per [RFC 7519 section 4.1.7.][rfc-7519-section-417] - - **MUST** be a value that ensures a negligible probability that the same value will appear on another JWT. - - **RECOMMENDED** be a UUIDv4 ([RFC 4122][rfc-4122]) compliant string. + * **OPTIONAL** as per RFC 7519 section 4.1.7 + * **MUST** be a value that ensures a negligible probability that the same value will appear on another JWT. + * **RECOMMENDED** be a UUIDv4 (RFC 4122) compliant string. * `nbf`: - - **OPTIONAL** as per [RFC 7519 section 4.1.5.][rfc-7519-section-415] - - **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. - - **MUST** be rejected if the current date/time is before the "not before" time. + * **OPTIONAL** as per RFC 7519 section 4.1.5 + * **MUST** be a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. + * **MUST** be rejected if the current date/time is before the "not before" time. * `sub`: - - **OPTIONAL** as per [RFC 7519 section 4.1.2.][rfc-7519-section-412] - - **RECOMMENDED** be the public key of the key pair used in the JWS, with a 4 byte checksum appended and encoded using base32 ([RFC 4648][rfc-4648-section-6]) encoding; the Algorand address, as defined in ([keys and addresses][algorand-keys-and-addresses]). - -[Back to top ^][title] + * **OPTIONAL** as per RFC 7519 section 4.1.2 + * **RECOMMENDED** be the public key of the key pair used in the JWS, with a 4 byte checksum appended and encoded using base32 (RFC 4648 section 6) encoding; the Algorand address, as defined in (keys and addresses). ### Signature -The signature is used to verify the message isn't changed along the way, and is signed with the private key of the Algorand account. +The signature is used to verify the message isn't changed along the way, and is signed with the private key of the Algorand account. -As defined in [RFC 7515 section 3][rfc-7515-section-3], the signature is a JSOE header, whose members are the union of the header and the payload, where each part **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding, concatenated with a dot (`.`), and then the resulting string is signed using the private key of the Algorand account. +As defined in RFC 7515 section 3, the signature is a JSOE header, whose members are the union of the header and the payload, where each part **MUST** be encoded using base64URL (RFC 4648 section 5) encoding, concatenated with a dot (`.`), and then the resulting string is signed using the private key of the Algorand account. -Finally, the signature's bytes **MUST** be encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding. +Finally, the signature's bytes **MUST** be encoded using base64URL (RFC 4648 section 5) encoding. ``` ed25529Sign( @@ -291,7 +280,7 @@ And a payload: } ``` -Encode each part using base64URL ([RFC 4648][rfc-4648-section-5]) encoding and concatenate with a dot (`.`) to produce: +Encode each part using base64URL (RFC 4648 Section 5) encoding and concatenate with a dot (`.`) to produce: ``` eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ0eXAiOiJKV1QiLCJ4IjoiRnJNVVkxLVU2ekxhNXh6M3IwUlVtSUxWd3F5SVkzMEhYNEpXRVhUTjJncms5RGpzIn0. @@ -300,14 +289,12 @@ eyJhdWQiOiJodHRwczovL2FwaS5hd2Vzb21lLmNvbSIsImV4cCI6MTcwNzc4MjQwMCwiaWF0IjoxNzA3 > ⚠️ **NOTE**: line breaks have been used in the above example. -Finally, the message can be signed using the private key, encoded using base64URL ([RFC 4648][rfc-4648-section-5]) encoding, to produce the following signature: +Finally, the message can be signed using the private key, encoded using base64URL (RFC 4648 Section 5) encoding, to produce the following signature: ``` wRUxp_9qLAutcXpzVaxKZpe3foQfQU2CDsIQZLXvVadhpDFN52ZYDHq3hk4C7bw65DetjRiaCCsPzj86I-QjDg== ``` -[Back to top ^][title] - ### A JSON Web Token Once the header, payload and signature have been created, the complete token is simply a concatenation of the three parts using a dot (`.`) as a delimiter: @@ -320,25 +307,19 @@ wRUxp_9qLAutcXpzVaxKZpe3foQfQU2CDsIQZLXvVadhpDFN52ZYDHq3hk4C7bw65DetjRiaCCsPzj86 > ⚠️ **NOTE**: line breaks have been used in the above example. -[Back to top ^][title] - ## Rationale The intention of this proposal is to live alongside other authentication methods, it does not aim to enforce a defacto authentication method for Algorand, but to merely outline a possible authentication method: JSON Web Token (JWT) within the context of an Algorand account. -[Back to top ^][title] - ## Reference Implementation > 🚨 **WARNING**: The below examples may use third party libraries and while they serve merely as examples, you should choose your own cryptographic implementation based on an implementation you trust and that has been audited. -[Back to top ^][title] - ### 1. A TypeScript Implementation Using TweetNaCl.js -The following example uses the popular [TweetNaCl.js][tweetnacl-js] library for cryptographic signing and verification. +The following example uses the popular TweetNaCl.js library for cryptographic signing and verification. -> ⚠️ **NOTE**: At the time of writing, [TweetNaCl.js][tweetnacl-js] was last audited between January-February 2017. The full audit report can be read [here](https://cure53.de/tweetnacl.pdf). +> ⚠️ **NOTE**: At the time of writing, TweetNaCl.js was last audited between January-February 2017. The full audit report can be read here. For creating and signing a JWT: @@ -379,7 +360,7 @@ json web token: eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ0eXAi */ ``` -To verify the JWT: +To verify the JWT: ```typescript import { decodeAddress, mnemonicToSecretKey } from 'algosdk'; @@ -402,8 +383,6 @@ is verified: true */ ``` -[Back to top ^][title] - ### 2. A Golang Implementation The following example uses golang's native crypto libraries. @@ -414,95 +393,95 @@ For creating and signing a JWT: package main import ( - "encoding/base64" - "encoding/json" - "fmt" - "github.com/algorand/go-algorand-sdk/v2/crypto" - "github.com/algorand/go-algorand-sdk/v2/mnemonic" - "golang.org/x/crypto/ed25519" - "log" - "time" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/mnemonic" + "golang.org/x/crypto/ed25519" + "log" + "time" ) type Header struct { - Algorithm string `json:"alg"` - Curve string `json:"crv,omitempty"` - KeyType string `json:"kty,omitempty"` - Type string `json:"typ,omitempty"` - PublicKey string `json:"x"` + Algorithm string `json:"alg"` + Curve string `json:"crv,omitempty"` + KeyType string `json:"kty,omitempty"` + Type string `json:"typ,omitempty"` + PublicKey string `json:"x"` } type RegisteredClaims struct { - Audience []string `json:"aud,omitempty"` - ExpiresAt time.Time `json:"exp,omitempty"` - ID string `json:"jti,omitempty"` - IssuedAt time.Time `json:"iat,omitempty"` - Issuer string `json:"iss,omitempty"` - NotBefore time.Time `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` + Audience []string `json:"aud,omitempty"` + ExpiresAt time.Time `json:"exp,omitempty"` + ID string `json:"jti,omitempty"` + IssuedAt time.Time `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore time.Time `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` } func main() string { - // get the address and private key from the 25-word mnemonic seed phrase - privateKey, err := mnemonic.ToPrivateKey("outside ancient world angry income move street brother patrol exist pet act banner quiz analyst gym build action dwarf direct castle coin fault absorb symptom") - if err != nil { - log.Fatal(err) - } - - account, err := crypto.AccountFromPrivateKey(privateKey) - if err != nil { - log.Fatal(err) - } - - address := account.Address.String() - - // encode the public in base64 url safe - encodedPublicKey := base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey)) - - header, err := json.Marshal(Header{ - Algorithm: "EdDSA", - Curve: "Ed25519", - KeyType: "OKP", - Type: "JWT", - PublicKey: encodedPublicKey, - }) - if err != nil { - log.Fatal(err) - } - - // encode the header in base64 url safe - encodedHeader := base64.RawURLEncoding.EncodeToString(header) - - payload, err := json.Marshal(RegisteredClaims{ - Audience: []string{"https://api.awesome.com"}, - ExpiresAt: time.Unix(1707782400, 0), - ID: "22080a89-a283-48e7-96c5-87f17ce7a850", - IssuedAt: time.Unix(1707696000, 0), - Issuer: "https://dapp.awesome.com", - NotBefore: time.Unix(1707739200, 0), - Subject: address, // 5QDXQXYN3INVOQZNW4EOJCP5HOZ55BO7OGQ5HTF4HUORY5HRLZYYLIY7MU - }) - if err != nil { - log.Fatal(err) - } - - // encode the payload in base64 url safe - encodedPayload := base64.RawURLEncoding.EncodeToString(payload) - - signature := ed25519.Sign( - privateKey, // sign with the algorand account's private key - []byte(fmt.Sprintf("%s.%s", encodedHeader, encodedPayload)), - ) - - // encode the signature in base64 url safe - encodedSignature := base64.RawURLEncoding.EncodeToString(signature) - - token := fmt.Sprintf("%s.%s.%s", encodedHeader, encodedPayload, encodedSignature) - - fmt.Println(fmt.Sprintf("json web token: %s", token)) - - /* - json web token: eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ0eXAiOiJKV1QiLCJ4IjoiN0FkNFh3M2FHMWRETGJjSTVJbjlPN1BlaGQ5eG9kUE12RDBkSEhUeFhuRSJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkuYXdlc29tZS5jb20iXSwiZXhwIjoiMjAyNC0wMi0xM1QwMDowMDowMFoiLCJqdGkiOiIyMjA4MGE4OS1hMjgzLTQ4ZTctOTZjNS04N2YxN2NlN2E4NTAiLCJpYXQiOiIyMDI0LTAyLTEyVDAwOjAwOjAwWiIsImlzcyI6Imh0dHBzOi8vZGFwcC5hd2Vzb21lLmNvbSIsIm5iZiI6IjIwMjQtMDItMTJUMTI6MDA6MDBaIiwic3ViIjoiNVFEWFFYWU4zSU5WT1FaTlc0RU9KQ1A1SE9aNTVCTzdPR1E1SFRGNEhVT1JZNUhSTFpZWUxJWTdNVSJ9.5S2MHI8LPC2cy5yv3ISNgulaEhpVk22JKyNxKi2J_uuqWCMacHgs27RuVlQbyipFlbc7z0p3AiRtxFcK8j-FCw - */ + // get the address and private key from the 25-word mnemonic seed phrase + privateKey, err := mnemonic.ToPrivateKey("outside ancient world angry income move street brother patrol exist pet act banner quiz analyst gym build action dwarf direct castle coin fault absorb symptom") + if err != nil { + log.Fatal(err) + } + + account, err := crypto.AccountFromPrivateKey(privateKey) + if err != nil { + log.Fatal(err) + } + + address := account.Address.String() + + // encode the public in base64 url safe + encodedPublicKey := base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey)) + + header, err := json.Marshal(Header{ + Algorithm: "EdDSA", + Curve: "Ed25519", + KeyType: "OKP", + Type: "JWT", + PublicKey: encodedPublicKey, + }) + if err != nil { + log.Fatal(err) + } + + // encode the header in base64 url safe + encodedHeader := base64.RawURLEncoding.EncodeToString(header) + + payload, err := json.Marshal(RegisteredClaims{ + Audience: []string{"https://api.awesome.com"}, + ExpiresAt: time.Unix(1707782400, 0), + ID: "22080a89-a283-48e7-96c5-87f17ce7a850", + IssuedAt: time.Unix(1707696000, 0), + Issuer: "https://dapp.awesome.com", + NotBefore: time.Unix(1707739200, 0), + Subject: address, // 5QDXQXYN3INVOQZNW4EOJCP5HOZ55BO7OGQ5HTF4HUORY5HRLZYYLIY7MU + }) + if err != nil { + log.Fatal(err) + } + + // encode the payload in base64 url safe + encodedPayload := base64.RawURLEncoding.EncodeToString(payload) + + signature := ed25519.Sign( + privateKey, // sign with the algorand account's private key + []byte(fmt.Sprintf("%s.%s", encodedHeader, encodedPayload)), + ) + + // encode the signature in base64 url safe + encodedSignature := base64.RawURLEncoding.EncodeToString(signature) + + token := fmt.Sprintf("%s.%s.%s", encodedHeader, encodedPayload, encodedSignature) + + fmt.Println(fmt.Sprintf("json web token: %s", token)) + + /* + json web token: eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ0eXAiOiJKV1QiLCJ4IjoiN0FkNFh3M2FHMWRETGJjSTVJbjlPN1BlaGQ5eG9kUE12RDBkSEhUeFhuRSJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkuYXdlc29tZS5jb20iXSwiZXhwIjoiMjAyNC0wMi0xM1QwMDowMDowMFoiLCJqdGkiOiIyMjA4MGE4OS1hMjgzLTQ4ZTctOTZjNS04N2YxN2NlN2E4NTAiLCJpYXQiOiIyMDI0LTAyLTEyVDAwOjAwOjAwWiIsImlzcyI6Imh0dHBzOi8vZGFwcC5hd2Vzb21lLmNvbSIsIm5iZiI6IjIwMjQtMDItMTJUMTI6MDA6MDBaIiwic3ViIjoiNVFEWFFYWU4zSU5WT1FaTlc0RU9KQ1A1SE9aNTVCTzdPR1E1SFRGNEhVT1JZNUhSTFpZWUxJWTdNVSJ9.5S2MHI8LPC2cy5yv3ISNgulaEhpVk22JKyNxKi2J_uuqWCMacHgs27RuVlQbyipFlbc7z0p3AiRtxFcK8j-FCw + */ } ``` @@ -512,22 +491,22 @@ Following on from the above example, we can add a simple function to verify the package main import ( - "encoding/base64" - "encoding/json" - "fmt" - // ... - "github.com/algorand/go-algorand-sdk/v2/types" - "golang.org/x/crypto/ed25519" - "log" - "strings" + "encoding/base64" + "encoding/json" + "fmt" + // ... + "github.com/algorand/go-algorand-sdk/v2/types" + "golang.org/x/crypto/ed25519" + "log" + "strings" ) type Header struct { - Algorithm string `json:"alg"` - Curve string `json:"crv,omitempty"` - KeyType string `json:"kty,omitempty"` - Type string `json:"typ,omitempty"` - PublicKey string `json:"x"` + Algorithm string `json:"alg"` + Curve string `json:"crv,omitempty"` + KeyType string `json:"kty,omitempty"` + Type string `json:"typ,omitempty"` + PublicKey string `json:"x"` } // ... @@ -536,86 +515,56 @@ func main() { isVerified := verify(address, token) - fmt.Println(fmt.Sprintf("is verified: %t", isVerified)) - /* - is verified: true - */ + fmt.Println(fmt.Sprintf("is verified: %t", isVerified)) + /* + is verified: true + */ } func verify(address string, token string) bool { - var decodedHeaderAddress types.Address - var header Header - - tokenParts := strings.Split(token, ".") - decodedHeader, err := base64.RawURLEncoding.DecodeString(tokenParts[0]) - if err != nil { - log.Fatal(err) - } - decodedSignature, err := base64.RawURLEncoding.DecodeString(tokenParts[2]) - if err != nil { - log.Fatal(err) - } - err = json.Unmarshal(decodedHeader, &header) - if err != nil { - log.Fatal(err) - } - decodedPublicKey, err := base64.RawURLEncoding.DecodeString(header.PublicKey) // get the raw public key; the "x" parameter, from the header - if err != nil { - log.Fatal(err) - } - - // get the address from the header's public key - copy(decodedHeaderAddress[:], decodedPublicKey) + var decodedHeaderAddress types.Address + var header Header + + tokenParts := strings.Split(token, ".") + decodedHeader, err := base64.RawURLEncoding.DecodeString(tokenParts[0]) + if err != nil { + log.Fatal(err) + } + decodedSignature, err := base64.RawURLEncoding.DecodeString(tokenParts[2]) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(decodedHeader, &header) + if err != nil { + log.Fatal(err) + } + decodedPublicKey, err := base64.RawURLEncoding.DecodeString(header.PublicKey) // get the raw public key; the "x" parameter, from the header + if err != nil { + log.Fatal(err) + } + + // get the address from the header's public key + copy(decodedHeaderAddress[:], decodedPublicKey) // ensure the address in the header matches an address you specify (e.g. this could be a user of your platform) - if address != decodedHeaderAddress.String() { + if address != decodedHeaderAddress.String() { fmt.Println(fmt.Sprintf("json invalid expected address '%s' but received '%s'", address, decodedHeaderAddress)) return false - } + } - return ed25519.Verify( - decodedPublicKey, - []byte(fmt.Sprintf("%s.%s", tokenParts[0], tokenParts[1])), // re-create the jose header that was signed using the header and payload - decodedSignature, - ) + return ed25519.Verify( + decodedPublicKey, + []byte(fmt.Sprintf("%s.%s", tokenParts[0], tokenParts[1])), // re-create the jose header that was signed using the header and payload + decodedSignature, + ) } ``` -[Back to top ^][title] - ## Security Considerations None. -[Back to top ^][title] - ## Copyright -Copyright and related rights waived via CCO. - -[Back to top ^][title] - - -[iana-jwa-list]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms -[algorand-keys-and-addresses]: https://developer.algorand.org/docs/get-details/accounts/#keys-and-addresses -[golang-jwt]: https://github.com/golang-jwt/jwt -[rfc-3986]: https://datatracker.ietf.org/doc/html/rfc3986 -[rfc-4122]: https://datatracker.ietf.org/doc/html/rfc4122 -[rfc-4648-section-5]: https://datatracker.ietf.org/doc/html/rfc4648#section-5 -[rfc-4648-section-6]: https://datatracker.ietf.org/doc/html/rfc4648#section-6 -[rfc-7515-section-3]: https://datatracker.ietf.org/doc/html/rfc7515#section-3 -[rfc-7515-section-411]: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1 -[rfc-7519]: https://datatracker.ietf.org/doc/html/rfc7519 -[rfc-7519-section-411]: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 -[rfc-7519-section-412]: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 -[rfc-7519-section-413]: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 -[rfc-7519-section-414]: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 -[rfc-7519-section-415]: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 -[rfc-7519-section-416]: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 -[rfc-7519-section-417]: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.57 -[rfc-8032]: https://datatracker.ietf.org/doc/html/rfc8032 -[rfc-8037]: https://datatracker.ietf.org/doc/html/rfc8037 -[rfc-8037-section-2]: https://datatracker.ietf.org/doc/html/rfc8037#section-2 -[rfc-8693-section-42]: https://datatracker.ietf.org/doc/html/rfc8693#section-4.2 -[title]: #authentication-with-json-web-token-jwt -[tweetnacl-js]: https://github.com/dchest/tweetnacl-js + +Copyright and related rights waived via CCO. \ No newline at end of file From 5727e7d5e7e7d4836b27e644223b7646e193fb92 Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Wed, 24 Apr 2024 15:29:18 +0100 Subject: [PATCH 11/11] chore: squash --- ARCs/arc-0080.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCs/arc-0080.md b/ARCs/arc-0080.md index e219dc749..6529030d0 100644 --- a/ARCs/arc-0080.md +++ b/ARCs/arc-0080.md @@ -21,7 +21,7 @@ The primary purpose of this proposal is to identify the subject of a JWT; the Al ## Motivation -Authentication is a wide-ranging subject and can be done in many different ways. JWTs have proven to be a hugely popular standard when it comes to authentication and, as Algorand accounts, at their core, are public/private key pairs using Ed25519 elliptic-curve signatures, they can easily be used to sign JWTs. +Authentication is a wide-ranging subject and can be done in many different ways. JWTs have proven to be a hugely popular standard when it comes to authentication and, as Algorand accounts, at their core, are public/private key pairs using Ed25519 elliptic-curve signatures; they can easily be used to sign JWTs. Furthermore, JWTs that are signed using an Algorand account's private key, can be used by clients (such as dApps) to prove the identity of a user and can open up clients to allow limited access to their services using methods such as "scope" (as is suggested in RFC 8693 section 4.2).