Abstract

This specification describes the authorization and authentication process between security-sensitive wallets and untrusted applications.

Since key-related resources should not be accessible to any untrusted applications, such as a website, until the user grants specific permissions on it, most of the endpoints should be guarded by an authentication system.

This specification defines some aspects of the authentication system, including:

  • Depiction of permission;
  • Two JSON-RPC methods to get and request permissions;
  • Two expressive user interfaces to inform permissions to grant and revoke;
  • Authorization process between application, wallet and user;
  • Authentication process between application and wallet;

Introduction

Wallets co-operating with untrusted applications are doing works viz.

  • Provide available addresses;
  • Provide live cells (including deps) for constructing transactions;
  • Switch between blockchain nodes to submit transactions;
  • Sign and send transactions proposed from applications;
  • Sign and verify messages from applications.

Some of them should be prohibited until the owner of the wallet grants related permissions on the requester.

For universal experience between a variety of applications and wallets, this specification proposes a canonical process for these tasks. Following this guide, developers can implement permission systems in a general way, and users can avoid mental burden on permission-grants between different applications.

Specification

Permission Restriction

This specification imposes three restrictions on each permission,

1
2
3
4
restrictions:
deps: string[] # required permissions for this permission
expiration: string | null # timestamp of expiration datetime, null for permanent
limit: string | null # limitation of invocation, null for unlimited

Permission Requests

This specification defines two requests in JSON-RPC method:

Get Permission List

get_permission_list queries the permission list provided by the wallet. On handling this kind of request, wallet should return the list of permissions in the following format.

1
2
3
4
5
6
permission_name:
is_granted: boolean # is granted or not
restriction:
deps: string[] # required permissions for this permission
expiration: string | null # timestamp of expiration datetime, null for permanent
limit: string | null # limitation of invocation, null for unlimited

Request Permissions

To request particular permissions, application should POST a request in following format:

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 1
jsonrpc: '2.0',
method: 'request_permissions',
params:
app:
name: string
description: string | null
permissions:
permission_name:
restriction:
expiration: string | null
limit: string | null
reason: string | null

The payload in this request indicates the details as follows:

  • Application is requesting permission_name permission
  • The permission_request should be available until expiration, eternal if expiration is null
  • The permission_request will be revoked automatically after limit invocations, null for unlimited.
  • Reason to request permission_name

Users should be informed with all this information in a proper way, including extra info about the requester.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌───────────────────────────────────────────────┐
│ ** Request for Permissions ** │
├───────────────────────────────────────────────┤
│ Application : app_name │
│ Description : app_description │
│ Origin : app_origin │
├───────────────────────────────────────────────┤
│ ** Requested Permissions ** │
├──────────┬─────────────────┬──────────────────┤
│ [x] name │ expiration time │ invocation limit │
│ ├-----------------┴------------------┤
│ │ Reason │
├──────────┼─────────────────┬──────────────────┤
│ [x] name │ expiration time │ invocation limit │
├──────────┴─────────────┬───┴──────────────────┤
│ Deny │ Grant │
└────────────────────────┴──────────────────────┘

With this interface, requested permissions can be granted as needed, which makes the permission system more flexible.

Once user confirms, wallet should return:

1
2
3
4
5
6
7
8
9
id: 1
jsonrpc: '2.0'
result:
permissions:
permission_name:
is_granted: boolean
message: string | null # reason for rejection, user rejected, permission unrecogonized, dep permissions are not granted, etc.
error: # unexpected error thrown in the procedure, like database error, etc
message: # expected exception, such as user denied, password incorrect, permission deps required

If user denies, the wallet should return:

1
2
3
4
5
id: 1
jsonrpc: '2.0'
result:
code: 401
message: permission request is denied

Revoke Permissions

A grant should be able to revoke freely in a kind way with essential annotations:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌───────────────────────────────────────────────┐
│ ** Revoke Permission ** │
├───────────────────────────────────────────────┤
│ Application : app_name │
│ Origin : app_origin │
│ Permission : permission_name │
├─────────────┬─────────────────────────────────┤
│ Dependents │ permission_name │
│ ├---------------------------------┤
│ │ permission_name │
├─────────────┴─────────┬───────────────────────┤
│ Cancel │ Confirm │
└───────────────────────┴───────────────────────┘

Authorization Process

The authorization process is a sequence of simple tasks that including application requests permissions, inform user necessary information, handle confirmation/rejection/exception.

sequenceDiagram
  participant Application
  participant Auth
  participant User

  Application ->> Auth: Request Permissions
  Auth -->> Application: If Exception Thrown
  Auth ->> User: App[x] Requests Permissions
  User ->> Auth: Pick Some or Deny
  Auth ->> Application: Permissions Granted
  Auth -->> Application: Request Denied

This specification has defined a method to request permissions and an advisable user interface to ask user for confirmation. Now the handlers on confirmation, rejection and exception should be complemented in this procedure.

There are three kinds of result in the end:

  1. Some permissions are granted;
  2. Request is denied;
  3. Exception occurs.

All these cases are covered by three fields of result of request_permissions defined above:

1
2
3
permissions: # result of request_permissions mentioned above, null for exception
error: # unexpected error thrown in the procedure, like database error, etc
message: # expected exception, such as user denied, password incorrect, permission deps required

error and message are worthy distinguishing since they describe different kinds of exception.

error means an unexpected exception is thrown, like database error or internal exception. While message indicates an expected behavior is delivered, e.g. user denied, or password is incorrect, etc.

With all of these definition, the workflow should be much clearer:

graph TD
App[Application] -->|Request| A{Auth}
A -->|Granted| G[permissions: non-null
error: null
message: null] A -->|Denied, Password Error, Dep Error| D[permissions: null
error: null
message: non-null] A -->|Unknown Exception| U[permissions: null
error: non-null
message: null]

Authentication Process[Discretionary]

Defining the process of authentication is good for writing SOLID code but it’s not mandatory in this system.

Name authentication mechanism Strategy, which is a concept from passport, and classify them into LocalStrategy, JwtStrategy, etc. by concrete mechanisms (More strategies can be found in Passport).

Suppose an endpoint get_xxx should be protected by the auth module auth.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
interface Verify {
(payload: string) : {
permissions: Permission[] | null,
error?: Error | null,
message?: string | null
}
}

class Strategy<T extends Verify> {

#name: string
#verify: T

get name() {
return this.#name
}

public constructor(name: string, verify: T) {
this.#verify = verify
this.#name = name
}

public validate(req: HttpRequest): ReturnType<Verify> {
const { payload } = this.getPayload(req)
return this.#verify(payload)
}

protected getPayload: (req: HttpRequest) => { payload: string }
}

// Register authentication strategy
auth.use(new Strategy(
'custom',
(payload: string) => {
const permissions = app.getPermissions(payload)
if (!permissions.xxx.is_granted) {
return {
permissions: null,
message: `Permission xxx is not granted`
}
}
return {
permissions,
}
}
))
// Protect endpoint with speicified strategy
app.post(`get_xxx`, auth.authenticate('custom'))

With this configuration, app invokes auth.validate on each post request before get_xxx route handler and if validation fails, the message will be passed to the router handler for the next step.

Conclusion

This specification illustrates several facets of authorization and authentication from perspectives of applications, wallets and users. With the APIs defined in this specification, developer experience is unified across different applications while with the request/revoke UIs pictured above, the user experience is also improved across various wallets.

References