Skip to content

Latest commit

 

History

History
422 lines (321 loc) · 27.4 KB

authentication-api.md

File metadata and controls

422 lines (321 loc) · 27.4 KB

Terms

  • Client App - the application which uses Orion as a backend, responsible for authenticating the user via the Auth API,
  • Auth API - Orion's authentication REST API, separate from the Orion GraphQL API
  • GraphQL API - Orion's GraphQL API, exposing all the GraphQL queries and mutations, accessible only by authenticated users,
  • User - any user of the Client App / Orion, regardless of whether they have a registered Gateway account or not,
  • Anonymous user - a user who either doesn't have a Gateway account or is not logged in to a Gateway account and therefore uses anonymous authentication.
  • Root user - a special kind of user, typically a gateway operator, with extra privileges to execute certain GraphQL API queries and mutations. It is initially created during database migration step based on the environment variables provided by the gateway administrator.
  • Gateway account owner - User that has registered and owns a Gateway account.
  • Authenticated request - a request which includes a valid session cookie (as described here) and can therefore be associated with an existing, active session (stored in Orion's database).
  • Authentication request - a request to perform the authentication and start a new session (either POST /login or POST /anonymous-auth).
  • Gateway account - an account that exists in Orion's database and can be logged in to, not to be confused with Blockchain account or Blockchain membership. Each Gateway account is associated with exactly one Blockchain account (which can be changed via POST /change-account endpoint) and exactly one Blockchain membership (which is immutable).
  • Blockchain account - an account that exists on the Joystream blockchain and can be identified with an address, such as j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf for example. A Blockchain account can be associated with exactly one Gateway account.
  • Blockchain membership - a membership created on a Joystream blockchain, which can be identified with a handle. A Blockchain membership can be associated with exactly one Gateway account.

Auth API overview

Orion's auth API is a REST API, separate from the GraphQL API (the main Orion API), which is being secured by it.

This approach can also be called out-of-band authenticaiton, to distinguish it from in-band authentication, which would be an authentication implemented as part of the same GraphQL api that is being secured by it.

The Auth API implementation can be found in the src/auth-server directory.

The implementation is based on the OpenAPI schema which can be found here.

The autogenerated Markdown documentation of the API can be found here. It is generated from the OpenAPI schema via npm run generate:docs:auth-api command.

Input schema

The input schema which defines the entities related to user authentication can be found in /schema/auth.graphql file.

User entity

User entity is the most basic representation of a Client App / Orion user, it can be either an anonymous user (have no related Account) or a gateway account owner.

Each User has a securely random id (32-byte string) assigned on creation, which can be stored on user's device (for example, in Browser's local storage) or shared across multiple devices in order to authenticate the user using anonymous authentication and preserve some information about their activity on the platform.

A User can be associated with activities such as viewing a video, or searching for specific content, which can be later used to provide a personalized experience to the user once they create an account.

Some example functionality that can be enabled for an anonymous Users (not all of those features are currently implemented):

  • Video view history
  • Continue watching...
  • Search history
  • Basic recommendations

We may choose not to provide all of those features to anonymous Users, but it should be possible to at least collect the user activity data, which can later be preserved once the user creates an account (and becomes a gateway account owner), because of the User <=> Account association.

Important: id of a User that has been associated with an Account can no longer be used to authenticate as anonymous user (ie. cannot be used for anonymous authentication)!

Session entity

Session represents a period of activity of a User that interacts with the Client App or Orion API directly, during which the user can perform authenticated requests (either as anonymous user or gateway account owner) and access the GraphQL API.

For more information about sessions see Sessions and authenticated requests.

Account entity

An Account represents a Gateway account which can be accessed by the Gateway account owner by providing a signed login message. The login message must be signed by the Blockchain account associated with the Gateway account, see: POST /login endpoint.

The current Blockchain account of a Gateway account is stored in the joystreamAccount field of the Account entity. It can be changed via POST /change-account endpoint.

The Blockchain membership associated with the Gateway account is stored in the membership field of the Account entity. It is assigned on account creation and cannot be changed.

EncryptionArtifacts entity

EncryptionArtifacts represents a set of encryption artifacts (cipherIv and encryptedSeed) which can be used by the Client app to decrypt the seed of a Blockchain account based on the account's login credentials (email and password). For details, see: Authentication API interactions (specifically Create user account and Login using e-mail and password flows).

The EncryptionArtifacts can be changed together with the Blockchain account associated with the Gateway account, see Change blockchain account & encryption artifacts associated with the gateway account flow.

SessionEncryptionArtifacts entity

SessionEncryptionArtifacts represents a set of encryption artifacts (cipherIv and cipherKey) associated with a given session, allowing the Client app to more securely store Blockchain account's seed throughout the session. For details, see Store encrypted seed for the duration of the session flow.

Token entity

Token represents a unique, securely random string generated by the Auth API for a given Account which can allow executing a specific action on behalf of the user without the need for authentication, Currently the only use-case for tokens is e-mail confirmation.

A token has an expiry date which depends on the Orion configuration (see: Configuration variables).

Configuration variables

Those configuration variables can be set as part of the environment, for more details about config variables see Config variables.

  • OPERATOR_SECRET - a secret string used as an identifier of the Root user, which is created during the database migration step. Important: Anyone who knows this secret can authenticate as the Root user (Gateway operator) and access the restricted queries and mutations!
  • SESSION_EXPIRY_AFTER_INACTIVITY_MINUTES - after how many minutes does the session expire in case they are no authenticated requests associated with the session being performed.
  • SESSION_MAX_DURATION_HOURS - after how many hours does the session expire regardless of whether there were any recent authenticated requests associated with the session performed.
  • SENDGRID_API_KEY - API key for the Sendgrid API, used for sending e-mails to the Gateway account owners by Orion (currently only for the purpose of e-mail confirmation)
  • SENDGRID_FROM_EMAIL - e-mail address that will be used as the sender of e-mails sent to the Gateway account owners by Orion.
  • APP_NAME - the name of the Gateway. It will be used in the e-mails sent to the Gateway account owners. It also has to be specified as part of the payload of some signed messages that need to be provided to the authentication api to make certain actions. For example, the log-in message which has to be provided in order to authenticate as Gateway account owner.
  • EMAIL_CONFIRMATION_ROUTE - the route in the Client app that will be used to confirm the e-mail address of a Gateway account owner.
  • EMAIL_CONFIRMATION_TOKEN_EXPIRY_TIME_HOURS - self-explainatory
  • EMAIL_CONFIRMATION_TOKEN_RATE_LIMIT - how many requests for a new e-mail confirmation token can be made within EMAIL_CONFIRMATION_TOKEN_EXPIRY_TIME_HOURS for a given e-mail address
  • ACCOUNT_OWNERSHIP_PROOF_EXPIRY_TIME_SECONDS - how many seconds have to pass since the timestamp included in a signed message that proves the ownership of a Blockchain account (ie. ActionExecutionRequestData) in order for that message to be considered expired.
  • COOKIE_SECRET - secret used to sign cookies, to make sure they come from Orion and have not been tampered with.

Sessions and authenticated requests

HttpOnly, same-site: strict session cookie is used as an authentication mechanism (both by the Auth API and the GraphQL API). This implies that the Client App, the GraphQL API and the Auth API must be hosted on the same domain!

The cookie is called session_id and stores the unique, randomly generated id of a Session entity in the database. It is set upon successful /login or /anonymous-auth request.

Upon receiving an authenticated request (ie. a request that contains a valid session_id cookie), the server reads session information associated with the session identified by the given session_id, either directly from the database (which is shared between GraphQL API server and the Auth API server) or from a memory session cache.

Each session, besides being associated with a specific user (either an anonymous user or Gateway account owner), includes the following information:

  • Session.ip - ip address of the agent that performed the authentication request,
  • Session.browser - browser that was used to perform the authentication request, as derived from the user-agent header,
  • Session.os - operating system that was used to perform the authentication request, as derived from the user-agent header,
  • Session.device - device that was used to perform the authentication request, as derived from the user-agent header,
  • Session.expiry - the date at which the session should expire or did expire.

This information is then compared with the authenticated request data. It is required that:

  • Session.ip matches the IP of the agent that made the authenticated request,
  • Session.browser, Session.os and Session.device match the values derived from the user-agent header included in the authenticated request,
  • Session.expiry is < Date.now().

This basically means that ip, brower, os and device should not change during the course of a given session. In case any of those change, a re-authentication is required.

This solution makes it possible to track the activity of a given User more accurately and adds additional layer of security, as even a stolen session cookie would be useless unless the attacker can make requests from the user's ip.

Session expiry

A session can expire:

  • If it is associated with an account and the account owner performs a POST /logout request,
  • If it is associated with anonymous user and the user creates a Gateway account,
  • After SESSION_EXPIRY_AFTER_INACTIVITY_MINUTES minutes of inactivity, counted from the last authenticated request,
  • After SESSION_MAX_DURATION_HOURS hours, starting from the time when the session was created.

Accessing the GraphQL API

Different requests may still require different privileges, ie. some mutations like setSupportedCategories will be only accessible for root user etc., while other mutations may only be accessible for Gateway account owners.

Authentication API interactions

Anonymous auth:

This is the first step required in order to interact with the GraphQL API.

POST /api/v1/anonymous-auth
  • Save userId from response to local storage and use it for subsequent guest auth requests once the session expires.

Auth as operator in order to access secured Orion queries and mutations:

POST /api/v1/anonymous-auth
{ "userId": "operator-secret" }

(where operator-secret must be the value of OPERATOR_SECRET environment variable)

Create email+password authenticated user account:

  1. Authenticate as anonymous user first (see Anonymous auth)

  2. Generate a random wallet seed and create encryption artifacts using user's credentials (e-mail, password). For reference code see the prepareEncryptionArtifacts implementation inside src/auth-server/tests/common.ts, ie.:

    export async function calculateLookupKey(email: string, password: string): Promise<string> {
      return (await scryptHash(`lookupKey:${email}:${password}`, 'lookupKeySalt')).toString('hex')
    }
    
    export async function prepareEncryptionArtifacts(
      seed: string,
      email: string,
      password: string
    ): Promise<EncryptionArtifacts> {
      // The `encryptionArtifacts.id` is deterministic:
      // It's an scrypt hash of the combination of user's e-mail and password
      // salted with some hardcoded `lookupKeySalt` value.
      const id = await calculateLookupKey(email, password)
      // The `encryptionArtifacts.cipherIv` is a random 16-byte value.
      const cipherIv = randomBytes(16)
      // The `cipherKey` is derived using a combination of user's e-mail and password.
      // `cipherIv` is used as an scrypt hash salt in this case.
      const cipherKey = await scryptHash(`cipherKey:${email}:${password}`, cipherIv)
      // The `seed` should be a random 16/32-byte value.
      // In this case we're using a string value, but you could also use a Buffer.
      // `encryptedSeed` is the result of encrypting the `seed` using `cipherKey` and `cipherIv`
      // with AES-256-CBC algorithm.
      const encryptedSeed = aes256CbcEncrypt(seed, cipherKey, cipherIv)
      return {
        id,
        cipherIv: cipherIv.toString('hex'),
        encryptedSeed,
      }
    }
  3. Create an on-chain membership using the address derived from the seed generated in the previous step as both controllerAccount and rootAccount. The easiest way to do that would be to use the Joystream membership faucet service.

  4. Make request to create a new account:

    POST /api/v1/account
    {
      "signature": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
      "payload": {
        "joystreamAccountId": "j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf",
        "memberId": "123",
        "gatewayName": "Gleev",
        "timestamp": 1682624588376,
        "action": "createAccount",
        "email": "user@example.com",
        "encryptionArtifacts": {
          "id": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
          "encryptedSeed": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
          "cipherIv": "0xffffffffffffffffffffffffffffffff"
        }
      }
    }
    

    Where:

    • signature is a signature over JSON.stringify(pyaload)
    • joystreamAccountId is the address of the keypair generated from seed (see step 2.)
    • memberId must be the id of the on-chain membership created in step 3. (the membership must be already processed by Orion)
    • gatewayName must match the APP_NAME environment variable
    • timestamp must be current timestamp in milliseconds
    • action must be createAccount
    • email must be the e-mail provided by the user in step 2.
    • encryptionArtifacts must be the same as the ones generated in step 2.

Log-in to user account using e-mail and password

  1. User provides email and password

  2. Compute id of the artifacts using the provided e-mail and password (see calculateLookupKey implementation in src/auth-server/tests/common.ts), ie.:

    export async function calculateLookupKey(email: string, password: string): Promise<string> {
      return (await scryptHash(`lookupKey:${email}:${password}`, 'lookupKeySalt')).toString('hex')
    }
  3. Get the artifacts:

    GET /api/v1/artifacts?id={id}&email={email}
    

    Where:

    • id is the id of the artifacts computed in step 2.
    • email is the e-mail provided by the user in step 1.

    In response you get the stored cipherIv and encryptedSeed.

  4. You can now decrypt user's seed using those artifacts, for reference code see decryptSeed implementation in src/auth-server/tests/common.ts, ie.:

    export async function decryptSeed(
      email: string,
      password: string,
      // Those are the values provided by the server as a response to `GET /api/v1/artifacts`:
      { cipherIv, encryptedSeed }: EncryptionArtifacts
    ): Promise<string> {
      const cipherIvBuf = Buffer.from(cipherIv, 'hex')
      // The `cipherKey` can be derived using a combination of user's e-mail, password and `cipherIv`.
      const cipherKey = await scryptHash(`cipherKey:${email}:${password}`, cipherIvBuf)
      // The `seed` can be decrypted using `cipherKey` and `cipherIv` with AES-256-CBC algorithm.
      return aes256CbcDecrypt(encryptedSeed, cipherKey, cipherIvBuf)
    }
  5. Make the login request:

    POST /api/v1/login
    {
      "signature": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
      "payload": {
        "joystreamAccountId": "j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf",
        "gatewayName": "Gleev",
        "timestamp": 1682624588376,
        "action": "login"
      }
    }
    

    Where:

    • signature is a signature over JSON.stringify(payload)
    • joystreamAccountId is the address of the account from the decrypted seed
    • gatewayName must match the APP_NAME environment variable
    • timestamp must be current timestamp in milliseconds
    • action must be login

    In response you'll get the accountId of the logged in account. You can always check the data associated with the logged in account using the accountData GraphQL query.

Store encrypted seed for the duration of the session

After the user is logged in, you can encrypt their wallet seed to store it more safely (for example: in local storage) for the duration of the session.

In order to do this:

  1. Generate random cipherIv (16 bytes) and cipherKey (32 bytes)
  2. Encrypt the seed using generated artifacts with AES-256-CBC algorithm
  3. Save session artifacts on the server:
    POST /api/v1/session-artifacts
    {
      "cipherKey": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
      "cipherIv": "0xffffffffffffffffffffffffffffffff"
    }
    
  4. You can retrieve the stored session artifacts later in order to decrypt the locally stored, encrypted seed:
    GET /api/v1/session-artifacts
    

Change Blockchain account / EncryptionArtifacts associated with the Gateway account

You can change the Blockchain account and remove or update EncryptionArtifacts associated with the Gateway account at the same time using POST /change-account endpoint.

There are 2 main use cases for this:

  1. Migrating from password-based authentication to a more secure external signer authentication: In this case you usually change the Blockchain account and remove the EncryptionArtifacts (ie. not provide the newArtifacts field in the request)
  2. Changing the account's password: In this case you can either change the EncryptionArtifacts, but keep the old Blockchain account or change both (in which case you need to migrate the assets and the membership to the new account first)
POST /api/v1/change-account
{
  "signature": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  "payload": {
    "joystreamAccountId": "j4UYhDYJ4pz2ihhDDzu69v2JTVeGaGmTebmBdWaX2ANVinXyE",
    "gatewayName": "Gleev",
    "timestamp": 1682624588376,
    "action": "changeAccount",
    "gatewayAccountId": "00000001",
    "newArtifacts": {
      "id": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
      "encryptedSeed": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
      "cipherIv": "0xffffffffffffffffffffffffffffffff"
    }
  }
}

Where:

  • signature is a signature over JSON.stringify(payload) (from the new Blockchain account)
  • joystreamAccountId is the address of the new Blockchain account (can be the same as the currently used one)
  • gatewayName must match the APP_NAME environment variable
  • timestamp must be current timestamp in milliseconds
  • action must be changeAccount
  • gatewayAccountId must be the accountId of the logged in Gateway account (as provided by the server in response to POST /api/v1/login or accountData GraphQL query)
  • newArtifacts optionally, the new EncryptionArtifacts if password-authentication is still being used

Retrieve logged-in Gateway Account data

Although this is not strictly done through the Auth API, it is worth mentioning that you can always retrieve the data of the currently logged-in Gateway Account by executing the following GraphQL query:

{
  accountData {
    id
    email
    joystreamAccount
    isEmailConfirmed
    membershipId
  }
}

Logout

POST /api/v1/logout

Development

Architecture

The central part of the Orion Auth Server is the OpenAPI schema (src/auth-server/openapi.yml). It defines the API endpoints, their parameters and responses.

The server itself is implemented using Express.js with express-openapi-validator middleware. The middleware is responsible for validating the requests (and optionally responses) against the OpenAPI schema and provides some other useful features like:

  • mapping requests to operation handler functions based on the x-eov-operation-handler property specified in the OpenAPI schema, so that individual routes don't have to be defined manually,
  • applying security handlers based on the security property specified in the OpenAPI schema.

Commands

  • npm run generate:types:auth-api - generates TypeScript types based on the OpenAPI schema. Those types are saved in src/auth-server/generated/api-types.ts.
  • npm run generate:docs:auth-api - generates the markdown documentation based on the OpenAPI schema. The documentation is saved in src/auth-server/docs.
  • npm run tests:auth-api - runs the auth API unit tests.

Making changes to the API

The process of introducing changes to the API usually involves:

  1. Making changes to the OpenAPI schema (src/auth-server/openapi.yml).
  2. Updating the autogenerated types and documentation by running npm run generate:types:auth-api and npm run generate:docs:auth-api.
  3. Making changes to the code to reflect the changes in the API. This usually involves adding or modifying the handlers in src/auth-server/handlers. The handlers are automatically connected to the API endpoints based on the x-eov-operation-handler property specified in the OpenAPI schema (ie. the filename of the handler must match the value of this property).
  4. Adding/adjusting the unit tests in src/auth-server/tests.
  5. Running the unit tests (npm run tests:auth-api) and manually testing the API (see the section below)

Testing

Unit tests

The unit tests are located in src/auth-server/tests. They are written using Mocha.js framework and supertest.

Generally for each API endpoint there should be at least one test case for each of the response codes defined in the OpenAPI schema.

Some common, reusable utilities and fixtures, like functions for doing anonymous auth, creating new accounts, signing in, verifying endpoint rate limits, encrypting and decrypting data etc. are located in src/auth-server/tests/common.ts.

Testing the API locally

You can use the OpenAPI playground (generated by swagger-ui-express package) to test the API locally.

To do that you need to set the OPENAPI_PLAYGROUND environment variable to true before starting the server. For example:

# If not already done, install dependencies, run codegen and build the code:
# make prepare

export OPENAPI_PLAYGROUND=true

docker-compose up -d orion_auth-api
# Or `make up` / `make up-squid` to run all the Orion services

By default the Auth API is served at http://localhost:4074/api/v1 and the playground is available at http://localhost:4074/playground.

Staging environment

When deploying to the staging environment, you can sidestep the same-site: strict and CORS restrictions in order to be able to test the API with the Client app (like Atlas) deployed under a different domain.

To do that you need to make sure to set those 2 environment variables:

export ORION_ENV=development
export DEV_DISABLE_SAME_SITE=true

Warning: Never use those settings in production! This configuration is much less secure and should only be used for testing purposes.

In order to be able to pass the cookie to Orion's Auth API when making requests from Atlas deployed under different domain, you should specify credentials: 'include' option in ApolloClient's HttpLink (see: https://www.apollographql.com/docs/react/networking/authentication/).

Similarly, to include the cookie when making requests to the GraphQL API, you should provide credentials: 'include' to fetch: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included