Skip to content

Commit

Permalink
Support local JWT auth in B2B sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
logan-stytch committed Aug 25, 2023
1 parent 94cbcab commit 19a22a1
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 16 deletions.
2 changes: 1 addition & 1 deletion lib/stytch/b2b_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def initialize(project_id:, secret:, env: nil, &block)
@organizations = StytchB2B::Organizations.new(@connection)
@passwords = StytchB2B::Passwords.new(@connection)
@sso = StytchB2B::SSO.new(@connection)
@sessions = StytchB2B::Sessions.new(@connection)
@sessions = StytchB2B::Sessions.new(@connection, project_id)
end

private
Expand Down
106 changes: 105 additions & 1 deletion lib/stytch/b2b_sessions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,31 @@
# or your changes may be overwritten later!
# !!!

require 'jwt'
require 'json/jwt'
require_relative 'errors'
require_relative 'request_helper'

module StytchB2B
class Sessions
include Stytch::RequestHelper

def initialize(connection)
def initialize(connection, project_id)
@connection = connection

@project_id = project_id
@cache_last_update = 0
@jwks_loader = lambda do |options|
@cached_keys = nil if options[:invalidate] && @cache_last_update < Time.now.to_i - 300
@cached_keys ||= begin
@cache_last_update = Time.now.to_i
keys = []
get_jwks(project_id: @project_id)['keys'].each do |r|
keys << r
end
{ keys: keys }
end
end
end

# Retrieves all active Sessions for a Member.
Expand Down Expand Up @@ -290,5 +307,92 @@ def get_jwks(
request = request_with_query_params("/v1/b2b/sessions/jwks/#{project_id}", query_params)
get_request(request)
end

# MANUAL(Sessions::authenticate_jwt)(SERVICE_METHOD)
# ADDIMPORT: require 'jwt'
# ADDIMPORT: require 'json/jwt'
# ADDIMPORT: require_relative 'errors'

# Parse a JWT and verify the signature. If max_token_age_seconds is unset, call the API directly
# If max_token_age_seconds is set and the JWT was issued (based on the "iat" claim) less than
# max_token_age_seconds seconds ago, then just verify locally and don't call the API
# To force remote validation for all tokens, set max_token_age_seconds to 0 or call authenticate()
def authenticate_jwt(
session_jwt,
max_token_age_seconds: nil,
session_duration_minutes: nil,
session_custom_claims: nil
)
if max_token_age_seconds == 0
return authenticate(
session_jwt: session_jwt,
session_duration_minutes: session_duration_minutes,
session_custom_claims: session_custom_claims
)
end

decoded_jwt = authenticate_jwt_local(session_jwt)
iat_time = Time.at(decoded_jwt['iat']).to_datetime
if iat_time + max_token_age_seconds >= Time.now
session = marshal_jwt_into_session(decoded_jwt)
{ 'session' => session }
else
authenticate(
session_jwt: session_jwt,
session_duration_minutes: session_duration_minutes,
session_custom_claims: session_custom_claims
)
end
rescue StandardError
# JWT could not be verified locally. Check with the Stytch API.
authenticate(
session_jwt: session_jwt,
session_duration_minutes: session_duration_minutes,
session_custom_claims: session_custom_claims
)
end

# Parse a JWT and verify the signature locally (without calling /authenticate in the API)
# Uses the cached value to get the JWK but if it is unavailable, it calls the get_jwks()
# function to get the JWK
# This method never authenticates a JWT directly with the API
def authenticate_jwt_local(session_jwt)
issuer = 'stytch.com/' + @project_id
begin
decoded_token = JWT.decode session_jwt, nil, true,
{ jwks: @jwks_loader, iss: issuer, verify_iss: true, aud: @project_id, verify_aud: true, algorithms: ['RS256'] }
decoded_token[0]
rescue JWT::InvalidIssuerError
raise JWTInvalidIssuerError
rescue JWT::InvalidAudError
raise JWTInvalidAudienceError
rescue JWT::ExpiredSignature
raise JWTExpiredSignatureError
rescue JWT::IncorrectAlgorithm
raise JWTIncorrectAlgorithmError
end
end

def marshal_jwt_into_session(jwt)
stytch_claim = 'https://stytch.com/session'
expires_at = jwt[stytch_claim]['expires_at'] || Time.at(jwt['exp']).to_datetime.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
# The custom claim set is all the claims in the payload except for the standard claims and
# the Stytch session claim. The cleanest way to collect those seems to be naming what we want
# to omit and filtering the rest to collect the custom claims.
reserved_claims = ['aud', 'exp', 'iat', 'iss', 'jti', 'nbf', 'sub', stytch_claim]
custom_claims = jwt.reject { |key, _| reserved_claims.include?(key) }
{
'session_id' => jwt[stytch_claim]['id'],
'user_id' => jwt['sub'],
'started_at' => jwt[stytch_claim]['started_at'],
'last_accessed_at' => jwt[stytch_claim]['last_accessed_at'],
# For JWTs that include it, prefer the inner expires_at claim.
'expires_at' => expires_at,
'attributes' => jwt[stytch_claim]['attributes'],
'authentication_factors' => jwt[stytch_claim]['authentication_factors'],
'custom_claims' => custom_claims
}
end
# ENDMANUAL(Sessions::authenticate_jwt)
end
end
4 changes: 1 addition & 3 deletions lib/stytch/magic_links.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,7 @@ def initialize(connection)
# Send a magic link to an existing Stytch user using their email address. If you'd like to create a user and send them a magic link by email with one request, use our [log in or create endpoint](https://stytch.com/docs/api/log-in-or-create-user-by-email).
#
# ### Add an email to an existing user
# This endpoint also allows you to add a new email to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in the request will add the email to the pre-existing Stytch User upon successful authentication.
#
# Adding a new email to an existing Stytch User requires the user to be present and validate the email via magic link. This requirement is in place to prevent account takeover attacks.
# This endpoint also allows you to add a new email address to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in your Send Magic Link by email request will add the new, unverified email address to the existing Stytch User. Upon successful authentication, the email address will be marked as verified.
#
# ### Next steps
# The user is emailed a magic link which redirects them to the provided [redirect URL](https://stytch.com/docs/guides/magic-links/email-magic-links/redirect-routing). Collect the `token` from the URL query parameters, and call [Authenticate magic link](https://stytch.com/docs/api/authenticate-magic-link) to complete authentication.
Expand Down
12 changes: 3 additions & 9 deletions lib/stytch/otps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,7 @@ def initialize(connection)
#
# ### Add a phone number to an existing user
#
# This endpoint also allows you to add a new phone number to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in the request will add the phone number to the pre-existing Stytch User upon successful authentication.
#
# Adding a new phone number to an existing Stytch User requires the user to be present and validate the phone number via OTP. This requirement is in place to prevent account takeover attacks.
# This endpoint also allows you to add a new phone number to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in your Send one-time passcode by SMS request will add the new, unverified phone number to the existing Stytch User. Upon successful authentication, the phone number will be marked as verified.
#
# ### Next steps
#
Expand Down Expand Up @@ -289,9 +287,7 @@ def initialize(connection)
#
# ### Add a phone number to an existing user
#
# This endpoint also allows you to add a new phone number to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in the request will add the phone number to the pre-existing Stytch User upon successful authentication.
#
# Adding a new phone number to an existing Stytch User requires the user to be present and validate the phone number via OTP. This requirement is in place to prevent account takeover attacks.
# This endpoint also allows you to add a new phone number to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in your Send one-time passcode by WhatsApp request will add the new, unverified phone number to the existing Stytch User. Upon successful authentication, the phone number will be marked as verified.
#
# ### Next steps
#
Expand Down Expand Up @@ -442,9 +438,7 @@ def initialize(connection)
# Send a One-Time Passcode (OTP) to a User using their email. If you'd like to create a user and send them a passcode with one request, use our [log in or create endpoint](https://stytch.com/docs/api/log-in-or-create-user-by-email-otp).
#
# ### Add an email to an existing user
# This endpoint also allows you to add a new email to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in the request will add the email to the pre-existing Stytch User upon successful authentication.
#
# Adding a new email to an existing Stytch User requires the User to be present and validate the email via OTP. This requirement is in place to prevent account takeover attacks.
# This endpoint also allows you to add a new email address to an existing Stytch User. Including a `user_id`, `session_token`, or `session_jwt` in your Send one-time passcode by email request will add the new, unverified email address to the existing Stytch User. Upon successful authentication, the email address will be marked as verified.
#
# ### Next steps
# Collect the OTP which was delivered to the user. Call [Authenticate OTP](https://stytch.com/docs/api/authenticate-otp) using the OTP `code` along with the `phone_id` found in the response as the `method_id`.
Expand Down
2 changes: 1 addition & 1 deletion lib/stytch/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def search(

# Update a User's attributes.
#
# **Note:** In order to add a new email address or phone number to an existing User object, pass the new email address or phone number into the respective `/send` endpoint for the authentication method of your choice. If you specify the existing User's `user_id` while calling the `/send` endpoint, the new email address or phone number will be added to the existing User object upon successful authentication. We require this process to guard against an account takeover vulnerability.
# **Note:** In order to add a new email address or phone number to an existing User object, pass the new email address or phone number into the respective `/send` endpoint for the authentication method of your choice. If you specify the existing User's `user_id` while calling the `/send` endpoint, the new, unverified email address or phone number will be added to the existing User object. Upon successful authentication, the email address or phone number will be marked as verified. We require this process to guard against an account takeover vulnerability.
#
# == Parameters:
# user_id::
Expand Down
2 changes: 1 addition & 1 deletion lib/stytch/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Stytch
VERSION = '6.4.0'
VERSION = '6.5.0'
end

0 comments on commit 19a22a1

Please sign in to comment.