-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
401 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# JWT usage | ||
|
||
## Generating JWT | ||
|
||
Use `signJwt` to generate a JWT signed by an account provided in arguments. | ||
```ts | ||
import { MemoryAccount, signJwt } from '@aeternity/aepp-sdk'; | ||
|
||
const account = MemoryAccount.generate(); | ||
const payload = { test: 'data' }; | ||
const jwt = await signJwt(payload, account); | ||
``` | ||
|
||
Provide `sub_jwk: undefined` in payload to omit signer public key added by default. | ||
Do it to make JWT shorter. | ||
```ts | ||
const jwt = await signJwt({ test: 'data', sub_jwk: undefined }, account); | ||
``` | ||
|
||
Or if you using a different way to encode a signer address. | ||
```ts | ||
const payload = { | ||
test: 'data', | ||
sub_jwk: undefined, | ||
address: 'ak_21A27UVVt3hDkBE5J7rhhqnH5YNb4Y1dqo4PnSybrH85pnWo7E', | ||
} | ||
const jwt = await signJwt(payload, account); | ||
``` | ||
|
||
## Verifying JWT | ||
|
||
Let's assume we got a JWT as string. Firstly we need to ensure that it has the right format. | ||
```ts | ||
import { isJwt, ensureJwt } from '@aeternity/aepp-sdk'; | ||
|
||
if (!isJwt(jwt)) throw new Error('Invalid JWT'); | ||
// alternatively, | ||
ensureJwt(jwt); | ||
``` | ||
|
||
After that we can pass JWT to other SDK's methods, for example to get JWT payload and signer address | ||
in case JWT has the signer public key included in `"sub_jwk"`. | ||
```ts | ||
import { unpackJwt } from '@aeternity/aepp-sdk'; | ||
|
||
const { payload, signer } = unpackJwt(jwt); | ||
console.log(payload); // { test: 'data', sub_jwk: { ... } } | ||
console.log(signer); // 'ak_21A27UVVt3hDkBE5J7rhhqnH5YNb4Y1dqo4PnSybrH85pnWo7E' | ||
``` | ||
`unpackJwt` will also check the JWT signature in this case. | ||
|
||
Alternatively, if `"sub_jwk"` is not included then we can provide signer address to `unpackJwt`. | ||
```ts | ||
const knownSigner = 'ak_21A27UVVt3hDkBE5J7rhhqnH5YNb4Y1dqo4PnSybrH85pnWo7E'; | ||
const { payload, signer } = unpackJwt(jwt, knownSigner); | ||
console.log(payload); // { test: 'data' } | ||
console.log(signer); // 'ak_21A27UVVt3hDkBE5J7rhhqnH5YNb4Y1dqo4PnSybrH85pnWo7E' | ||
``` | ||
|
||
If we need to a get signer address based on JWT payload then we need to unpack it without checking | ||
the signature. Don't forget to check signature after that using `verifyJwt`. | ||
```ts | ||
import { verifyJwt } from '@aeternity/aepp-sdk'; | ||
|
||
const { payload, signer } = unpackJwt(jwt); | ||
console.log(payload); // { address: 'ak_21A27UVVt3hDkBE5J7rhhqnH5YNb4Y1dqo4PnSybrH85pnWo7E' } | ||
console.log(signer); // undefined | ||
if (!verifyJwt(jwt, payload.address)) throw new Error('JWT signature is invalid'); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<template> | ||
<h2>Generate a JWT</h2> | ||
<div class="group"> | ||
<div> | ||
<div>Payload as JSON</div> | ||
<div> | ||
<input | ||
:value="payloadAsJson" | ||
@input="payloadAsJson = $event.target.value || '{}'" | ||
> | ||
</div> | ||
</div> | ||
<div> | ||
<div>Include "sub_jwk"</div> | ||
<div> | ||
<input | ||
type="checkbox" | ||
v-model="includeSubJwk" | ||
> | ||
</div> | ||
</div> | ||
<button @click="() => { signPromise = sign(); }"> | ||
Sign | ||
</button> | ||
<div v-if="signPromise"> | ||
<div>Signed JWT</div> | ||
<Value :value="signPromise" /> | ||
</div> | ||
</div> | ||
|
||
<h2>Unpack and verify JWT</h2> | ||
<div class="group"> | ||
<div> | ||
<div>JWT to unpack</div> | ||
<div> | ||
<input | ||
:value="jwt" | ||
@input="jwt = $event.target.value || null" | ||
> | ||
</div> | ||
</div> | ||
<div> | ||
<div>Signer address</div> | ||
<div> | ||
<input | ||
:value="address" | ||
@input="address = $event.target.value || null" | ||
> | ||
</div> | ||
</div> | ||
<button @click="() => { unpackPromise = unpack(); }"> | ||
Unpack | ||
</button> | ||
<div v-if="unpackPromise"> | ||
<div>Unpack result</div> | ||
<Value :value="unpackPromise" /> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
import { mapState } from 'vuex'; | ||
import { unpackJwt, signJwt } from '@aeternity/aepp-sdk'; | ||
import Value from './components/Value.vue'; | ||
export default { | ||
components: { Value }, | ||
computed: mapState(['aeSdk']), | ||
data: () => ({ | ||
payloadAsJson: '{ "test": true }', | ||
includeSubJwk: true, | ||
signPromise: null, | ||
jwt: 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWJfandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaEF5WFM1Y1dSM1pGUzZFWjJFN2NUV0JZcU43SksyN2NWNHF5MHd0TVFnQSJ9LCJ0ZXN0IjoiZGF0YSJ9.u9El4b2O2LRhvTTW3g46vk1hx0xXWPkJEaEeEy-rLzLr2yuQlNc7qIdcr_z06BgHx5jyYv2CpUL3hqLpc0RzBA', | ||
address: null, | ||
unpackPromise: null, | ||
}), | ||
methods: { | ||
async sign() { | ||
const payload = JSON.parse(this.payloadAsJson); | ||
if (!this.includeSubJwk) payload.sub_jwk = undefined; | ||
// TODO: expose account used in aepp-wallet connection | ||
return signJwt(payload, this.aeSdk._resolveAccount(this.aeSdk.address)); | ||
}, | ||
async unpack() { | ||
return unpackJwt(this.jwt, this.address); | ||
}, | ||
}, | ||
}; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import canonicalize from 'canonicalize'; | ||
import AccountBase from '../account/Base'; | ||
import { | ||
Encoded, Encoding, decode, encode, | ||
} from './encoder'; | ||
import { verify } from './crypto'; | ||
import { ArgumentError, InvalidSignatureError } from './errors'; | ||
|
||
// TODO: use Buffer.from(data, 'base64url') after solving https://github.com/feross/buffer/issues/309 | ||
const toBase64Url = (data: Buffer | Uint8Array | string): string => Buffer | ||
.from(data) | ||
.toString('base64') | ||
.replaceAll('/', '_') | ||
.replaceAll('+', '-') | ||
.replace(/=+$/, ''); | ||
|
||
const fromBase64Url = (data: string): Buffer => Buffer | ||
.from(data.replaceAll('_', '/').replaceAll('-', '+'), 'base64'); | ||
|
||
const objectToBase64Url = (data: any): string => toBase64Url(canonicalize(data) ?? ''); | ||
|
||
const header = 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9'; // objectToBase64Url({ alg: 'EdDSA', typ: 'JWT' }) | ||
|
||
/** | ||
* JWT including specific header | ||
* @category JWT | ||
*/ | ||
export type Jwt = `${typeof header}.${string}.${string}`; | ||
|
||
/** | ||
* Generate a signed JWT | ||
* Provide `"sub_jwk": undefined` in payload to omit signer public key added by default. | ||
* @param originalPayload - Payload to sign | ||
* @param account - Account to sign by | ||
* @category JWT | ||
*/ | ||
export async function signJwt(originalPayload: any, account: AccountBase): Promise<Jwt> { | ||
const payload = { ...originalPayload }; | ||
if (!('sub_jwk' in payload)) { | ||
payload.sub_jwk = { | ||
kty: 'OKP', | ||
crv: 'Ed25519', | ||
x: toBase64Url(decode(account.address)), | ||
}; | ||
} | ||
if (payload.sub_jwk === undefined) delete payload.sub_jwk; | ||
const body = `${header}.${objectToBase64Url(payload)}` as const; | ||
const signature = await account.sign(body); | ||
return `${body}.${toBase64Url(signature)}`; | ||
} | ||
|
||
/** | ||
* Unpack JWT. It will check signature if address or "sub_jwk" provided. | ||
* @param jwt - JWT to unpack | ||
* @param address - Address to check signature | ||
* @category JWT | ||
*/ | ||
export function unpackJwt(jwt: Jwt, address?: Encoded.AccountAddress): { | ||
/** | ||
* JWT payload as object | ||
*/ | ||
payload: any; | ||
/** | ||
* Undefined returned in case signature is not checked | ||
*/ | ||
signer: Encoded.AccountAddress | undefined; | ||
} { | ||
const components = jwt.split('.'); | ||
if (components.length !== 3) throw new ArgumentError('JWT components count', 3, components.length); | ||
const [h, payloadEncoded, signature] = components; | ||
if (h !== header) throw new ArgumentError('JWT header', header, h); | ||
const payload = JSON.parse(fromBase64Url(payloadEncoded).toString()); | ||
const jwk = payload.sub_jwk ?? {}; | ||
const signer = jwk.x == null || jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' | ||
? address | ||
: encode(fromBase64Url(jwk.x), Encoding.AccountAddress); | ||
if (address != null && signer !== address) { | ||
throw new ArgumentError('address', `${signer} ("sub_jwk")`, address); | ||
} | ||
if ( | ||
signer != null | ||
&& !verify(Buffer.from(`${h}.${payloadEncoded}`), fromBase64Url(signature), signer) | ||
) { | ||
throw new InvalidSignatureError(`JWT is not signed by ${signer}`); | ||
} | ||
return { payload, signer }; | ||
} | ||
|
||
/** | ||
* Check is string a JWT or not. Use to validate the user input. | ||
* @param maybeJwt - A string to check | ||
* @returns True if argument is a JWT | ||
* @category JWT | ||
*/ | ||
export function isJwt(maybeJwt: string): maybeJwt is Jwt { | ||
try { | ||
unpackJwt(maybeJwt as Jwt); | ||
return true; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Throws an error if argument is not JWT. Use to ensure that a value is JWT. | ||
* @param maybeJwt - A string to check | ||
* @category JWT | ||
*/ | ||
export function ensureJwt(maybeJwt: string): asserts maybeJwt is Jwt { | ||
unpackJwt(maybeJwt as Jwt); | ||
} | ||
|
||
/** | ||
* Check is JWT signed by address from arguments or "sub_jwk" | ||
* @param jwt - JWT to check | ||
* @param address - Address to check signature | ||
* @category JWT | ||
*/ | ||
export function verifyJwt(jwt: Jwt, address?: Encoded.AccountAddress): boolean { | ||
try { | ||
const { signer } = unpackJwt(jwt, address); | ||
return signer != null; | ||
} catch (error) { | ||
return false; | ||
} | ||
} |
Oops, something went wrong.