Skip to main content

Authentication between Applications

A JWT is the way chosen in Treatwell to create the Bearer Token used in the Bearer Authentication to authenticate trusted sources.

That means that sources that wanna call TWPro APIs have to create a JSON Web Token with a specific content. And when TWPro wants to call another Treatwell service's APIs, it should sign a JWT with a specific content.

JWT (JSON Web Token) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

The Algorithm used in TWPro to sign the JWT is ES512.

JSON Web Token content​

The content of the JSON Web Token used on TWPro to define it's validity are:

  • issuer: it's the URL of the source that "created" the token and signed it with its private key.
  • exp: it's the timestamp that define the expiration date of the JWT
  • nbf: it's the timestamp that define since when the JWT is valid
  • kid: key ID is a string that indicates the key that was used to digitally sign the JWT

Validate JWT Token from another Application​

To validate that an API is received from a trusted source it means that the JWT Token sent as Bearer Token is valid.

To validate a JWT token it means that it can be decoded using a public cryptographic key (the public counterpart of the private key used to sign the JWT). And the JSON contained is the expected one.

For further details it's possible to read the first comment on Uala::Jwt::Validator that explains how it's implemented in TWPro.

!ref Example Pull Request to validate a new Application

Public Key Retrieval​

To search the public key the steps are:

A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key. This specification also defines a JWK Set JSON data structure that represents a set of JWKs. Cryptographic algorithms and identifiers for use with this specification are described in the separate JSON Web Algorithms (JWA) specification and IANA registries established by that specification

Static public key​

Static public keys are stored in filesystem using this structure, default validator instance reads public keys from Rails.root.join('config/public_keys')

public_keys
├── <application-1>
│ ├── <kid-1>
│ │ ├── dev.pem
│ │ ├── prod.pem
│ │ └── staging.pem
│ └── <kid-2>
└── <application-2>
└── <kid>

!ref Generate public keys in PEM format

Published public keys according to OpenId spec​

To generate a private key using ES512 and the JSON Web Key parameters it's possible to use jwcrypto or docker

Paying attention that those libraries won't steal the private key generated (using the docker should be easier to secure because it can be deleted right after the creation)

New issuer​

To validate the JWT token the application and the environment (dev prod staging) are retrieved from the issuer defined inside JWT token. To extract those values from the issuer and validate that it's format a regex is used. That regex should be added to Uala::Jwt::Validator::ISSUER_REGEX

Example:

TREATWELL_PARTNERS_ISSUER = %r{^https://partners\.treatwell\.net(?:\.(?<env>.+)\.twtest\.io)?/api/auth/partner/(?<application>[^/]+)$}

JWT Validation on Controller EP​

Then to define that a controller uses a JWT Authentication it should be like:

class YourController < ApplicationController
include Uala::Jwt::ControllerConcern
has_jwt_authentication! <application_name>
end

has_jwt_authentication decodes the JWT token using the public key expected from it's not signed JSON and then it checks that those are equal to the the signed ones. And the application_name is the same as application extracted from the issuer stored in the JWT (the issuer is the URL that identify the application that signed the JWT).

Warning: Uala::Jwt::ControllerConcern instantiate the application as a class variable, that could create some problems if a Controller include the module it's children want to use different application names (see https://www.codegram.com/blog/understanding-class-instance-variables-in-ruby/ or https://medium.com/@jeffm.adler/inheriting-class-variables-in-ruby-971f8f977884)

Generate JWT Token​

The class Uala::Jwt::Generator is used to generate valid a JWT token.

Generate a JWT Token means to create a JSON with the fields expected by the API's provider and sign it with the the API's consumer private key. Then the API's provider will use the expected public key to decode and verify the signed content and it's validity.

Generate and Validate same JWT Token​

In that scenario the TWPro SaaS generate a JWT Token using it's private key and then it should validate that the JWT Token received back is valid using it's public key. In that case issuer and kid should follow the same rules applied to an external Application

!ref Example Generate and validate token from TWPro SaaS

Impersonation Token and flow​

What is an Impersonation Token?​

Impersonation Token at Treatwell is a JWT token generated in a specific format for Admin users (in ActiveAdmin) to log in as a normal user to Pro systems

Impersonation flow​

  1. Impersonation Token generation
  1. Impersonation Token parsing

Impersonation flow in codebase​

  1. Entry point uala-admin/app/admin/user.rb L531: action_item :impersonate_dev, only: :show
  2. Impersonation endpoint uala-admin/app/admin/user.rb L537: member_action :impersonate, method: :put
  3. Uala::Jwt::Impersonator(Deprecated) uala-core/lib/uala/jwt/impersonator.rb: Generate and parse JWT token
  4. Uala::Jwt::ImpersonationTokenGenerator uala-core/lib/uala/jwt/impersonation_token_generator.rb: Generate JWT token
  5. Uala::Jwt::ImpersonationTokenParser uala-core/lib/uala/jwt/impersonation_token_parser.rb: Parse JWT token
  6. Uala::Jwt::ImpersonationTokenHost uala-core/lib/uala/jwt/impersonation_token_host.rb: Determine which env should be used for issuer while generating/parsing tokens. This is where went wrong when the Dev domain changed from uala.dev to twpro-dev.twtest.io
  7. Api::V1::Manager::BaseController uala-main/app/controllers/api/v1/manager/base_controller.rb: extra authentication for Old Statistic (The one inside uala-main), which makes extra calls to /authorize.json
  8. Auth::V1::AuthorizationController uala-auth/app/controllers/auth/v1/authorization_controller.rb: API endpoint for above requests
  9. Statistics::V1::BaseController uala-statistics/app/controllers/statistics/v1/base_controller.rb: the New Statistic, which makes request to ENV["AUTHENTICATION_URL"], defined in uala-iac as K8s internal domain

Useful debugging tips​

  1. https://jwt.io/ is useful to decode tokens and verify its content
  2. https://rubular.com/ is useful to determine which env is used from request host