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 JWTnbf: it's the timestamp that define since when the JWT is validkid: 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
- Impersonation Token generation
- Impersonation Token parsing
Impersonation flow in codebase
- Entry point
uala-admin/app/admin/user.rb L531:action_item :impersonate_dev, only: :show - Impersonation endpoint
uala-admin/app/admin/user.rb L537:member_action :impersonate, method: :put Uala::Jwt::Impersonator(Deprecated)uala-core/lib/uala/jwt/impersonator.rb: Generate and parse JWT tokenUala::Jwt::ImpersonationTokenGeneratoruala-core/lib/uala/jwt/impersonation_token_generator.rb: Generate JWT tokenUala::Jwt::ImpersonationTokenParseruala-core/lib/uala/jwt/impersonation_token_parser.rb: Parse JWT tokenUala::Jwt::ImpersonationTokenHostuala-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 fromuala.devtotwpro-dev.twtest.ioApi::V1::Manager::BaseControlleruala-main/app/controllers/api/v1/manager/base_controller.rb: extra authentication for Old Statistic (The one insideuala-main), which makes extra calls to/authorize.jsonAuth::V1::AuthorizationControlleruala-auth/app/controllers/auth/v1/authorization_controller.rb: API endpoint for above requestsStatistics::V1::BaseControlleruala-statistics/app/controllers/statistics/v1/base_controller.rb: the New Statistic, which makes request to ENV["AUTHENTICATION_URL"], defined inuala-iacas K8s internal domain
Useful debugging tips
- https://jwt.io/ is useful to decode tokens and verify its content
- https://rubular.com/ is useful to determine which env is used from request host