-
Notifications
You must be signed in to change notification settings - Fork 0
Authentication
RailsIAM provides an JWT-based authentication system designed for API applications. It handles access tokens, refresh tokens, session management, session strategy, and authentication strategies while keeping authentication and authorization cleanly separated.
To enable RailsIAM authentication, add the :authentication macro to your base controller:
rails_iam :authenticationIf your application already uses Devise or another authentication solution, you can continue using it. RailsIAM only requires your authentication model and a method that returns the currently authenticated user (for example, current_user).
If you choose to use RailsIam's authentication, it provides:
- JWT access tokens
- Refresh token rotation
- HttpOnly cookie support
- Bearer token support
- Token extraction
- Configurable JWT claims
- Secure password authentication with BCrypt
Once you enable :authentication, this automatically installs a before_action that performs authentication for every request.
If authentication fails at any stage, RailsIAM raises an RailsIam::Authentication::Exceptions::AuthenticationError.
Endpoints
RailsIAM provides three authentication endpoints out of the box:
| Endpoint | Purpose |
|---|---|
auth/sign_in |
Authenticate a user and issue access/refresh tokens |
auth/sign_out |
Sign out the current user and revoke the session |
auth/refresh |
Generate a new access token using a valid refresh token |
These three routes will be mounted inside your host application. No additional controller or route configuration is required from your host application.
If you prefer a different URL structure, you can customize the endpoint paths through the RailsIAM configuration.
RailsIAM also provides a way to skip authentication when certain controllers or actions need to be publicly accessible.
-
skip_authentication— skips authentication for the entire controller. -
skip_authentication only: []— skips authentication only for the specified actions. -
skip_authentication except: []— skips authentication for all actions except the specified actions.
Authentication and authorization are separate concerns. Skipping authentication only controls whether a request requires an authenticated user. It does not disable authorization checks unless skip_authorization is also applied.
The Login service authenticates a user using their credentials and creates a new authenticated session.
RailsIam::Authentication::Login.call( email: params[:email], password: params[:password])On successful authentication, the service:
- Authenticate user by email and password.
- Generates a short-lived JWT access token
- Creates and stores a refresh token
- Applies the configured session strategy
- Returns the authenticated user along with the generated tokens
Session Strategy
RailsIAM supports two session strategies.
:single
By default only one active session is allowed per user.
When the user signs in from a new device or browser, all previously active refresh tokens are revoked, effectively signing the user out from other devices.
:multi
Multiple active sessions are allowed.
Signing in from another device creates an additional session without affecting existing ones.
Authentication Strategy
RailsIAM supports multiple ways of delivering the access token.
:cookie
The access token is stored as a secure HttpOnly cookie, making it suitable for browser-based applications. Its default strategy.
:bearer
The access token is returned in the JSON response and is expected to be sent in the Authorization header on subsequent requests.
curl --location 'localhost:3000/protected-routes' \
--header 'Authorization: Bearer token....'
Both strategies can also be enabled together if your application requires them.
Token Lifetime
By default, the access token expires after 15 minutes and refresh token after 30 days.
The expiration time is fully configurable from the RailsIAM configuration.
JwtEncoder is responsible for generating short-lived JWT access tokens for authenticated users.
By default, RailsIAM generates a token with the following claims:
claims = {
sub: user.id,
email: user.email,
exp: ..., # Expiration time (configurable)
iat: ..., # Issued at
jti: ..., # SecureRandom.uuid
}Every generated access token includes a unique token identifier (jti).
When you authenticate using the built in RailsIAM Login service, the same jti is also stored alongside the generated refresh token in the rails_iam_refresh_tokens table.
Although RailsIAM does not implement access token blacklisting out of the box, storing the jti makes it straightforward to build features such as:
- Access token revocation
- Token blacklisting
- Forced logout from all devices
- Security auditing
You are not required to use RailsIam's built in Login service.
For example, your application may authenticate users using a username instead of an email address. You can add username with unique index in rails_iam_users.
user = User.find_by!(username: params[:username])
raise RailsIam::Authentication::Exceptions::AuthenticationError unless user.authenticate(params[:password])
token = RailsIam::Authentication::JwtEncoder.call(user, jti: SecureRandom.uuid) # jti requiredIn this case, RailsIAM is only responsible for generating the JWT after your authentication logic succeeds.
JwtEncoder allows additional claims and JWT headers to be supplied when generating a token.
extra_claims = {
jti: SecureRandom.uuid,
nbf: Time.current.to_i,
roles: user.role_names,
permissions: user.permission_codes
}
headers = {
token: "jwt"
}
token = RailsIam::Authentication::JwtEncoder.call(user, extra_claims, headers)Internally, RailsIAM merges the default claims with your custom claims before encoding the token.
JWT.encode(default_claims.merge(extra_claims), jwt_secret_key, jwt_algorithm, headers)You can also include standard JWT claims such as iss (issuer) and aud (audience) either globally through the RailsIAM configuration, or dynamically by passing them as additional claims.
The following JWT settings are configurable through RailsIAM configuration: secret key, signing algorithm, token expiration time, issuer (iss), audience (aud)
Access tokens are intentionally short-lived to reduce security risks.
When an access token expires, the client can exchange a valid refresh token for a new access token without requiring the user to sign in again.
During the refresh process, RailsIam:
- Extracts the refresh token from the configured transport
refresh_token_transport. - Locates the corresponding session using the token digest.
- Validates that the refresh token is valid.
- Generates a new JWT access token.
- Updates the refresh token's
last_used_attimestamp and JWT identifier (jti). - Returns the new access token using the configured authentication strategy.
Refresh Token Storage
RailsIam::Authentication::RefreshTokenEncoder.call generates a cryptographically secure token using:
SecureRandom.hex(64)For security reasons, the raw refresh token is never stored in the database.
Instead, RailsIAM stores a SHA256 digest of the token together with session metadata.
Each refresh token record contains:
| Column | Description |
|---|---|
token_digest |
SHA256 digest used to verify the refresh token |
jti |
JWT identifier associated with the current access token |
expires_at |
Refresh token expiration time |
last_used_at |
Timestamp of the most recent successful refresh |
ip_address |
Client IP address captured during sign in |
user_agent |
Client user agent captured during sign in |
Refresh Token Transport
RailsIAM supports multiple ways of sending the refresh token based on your configured authentication_strategies
When authentication strategy is cookie :
When using cookie authentication, the refresh token is automatically read from an encrypted HttpOnly cookie.
No additional client code is required.
When authentication strategy is bearer :
For bearer strategy you have two options:
Request Header
You may configure the refresh token to be sent using a custom request header. Reading it from header is default behavior.
X-Refresh-Token: <refresh_token>
curl --location --request POST 'localhost:3000/auth/refresh' \
--header 'refresh_token: ....'
Request Body
For API clients that prefer request payloads, the refresh token can be submitted in the request body.
curl --location '/auth/refresh' \
--header 'Content-Type: application/json' \
--data '{
"refresh_token": "..."
}
The transport mechanism is configured through refresh_token_transport.
JwtDecoder validates incoming JWTs.
During decoding it verifies:
- JTI, token issuer
- Signature
- Expiration
- Token format
- Required claims
Once validated, the decoder returns the payload.
payload = RailsIam::Authentication::JwtDecoder.call(token, headers={})
JwtDecoder.call(token, headers={}) internally checks
JWT.decode(token, secret, true, { algorithm: algo, verify_jti: true }.merge(headers),)
The authenticated user is then loaded using the configured user model.
After successful authentication, the authenticated user is available throughout the request.
RailsIam::Authentication::Current.user
Authorization, auditing, and other RailsIAM components use this object internally.
Signing out terminates the authenticated session by revoking the associated refresh token.
RailsIam::Authentication::Logout.call(
refresh_token: current_refresh_token
)During sign out, RailsIam:
- Locates the current refresh token.
- Delete the refresh token
- Removes the authentication cookies (access and refresh) when using the
:cookiestrategy. - Returns 204 No Content.
If your authentication strategy is :bearer, you need to send refresh_token using via header or request_body as per refresh_token_transport configuration.
Once signed out, the refresh token can no longer be used to obtain new access tokens. Any existing access token remains valid only until its expiration.
Authentication failures raise:
RailsIam::Authentication::Exceptions::AuthenticationError
The engine provides a default JSON response for API applications.
Host applications can override this behavior by adding following method in application controller.
def render_rails_iam_authentication_error(exception)
render json: {
message: 'your custom message',
status: 401,
}, status: :unauthorized
end