Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ gem "trilogy", "~> 2.9"

# Features
gem "bcrypt", "~> 3.1.7"
gem "omniauth_openid_connect", "~> 0.8.0"
gem "omniauth-rails_csrf_protection", "~> 1.0"
gem "geared_pagination", "~> 1.2"
gem "rqrcode"
gem "redcarpet"
Expand Down
73 changes: 73 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ GEM
railties
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
ast (2.4.3)
attr_required (1.0.2)
autotuner (1.1.0)
aws-eventstream (1.4.0)
aws-partitions (1.1203.0)
Expand All @@ -146,6 +148,7 @@ GEM
bcrypt_pbkdf (1.1.2)
benchmark (0.5.0)
bigdecimal (4.0.1)
bindata (2.5.1)
bindex (0.8.1)
bootsnap (1.20.1)
msgpack (~> 1.2)
Expand Down Expand Up @@ -180,12 +183,22 @@ GEM
dotenv (3.2.0)
drb (2.2.3)
ed25519 (1.4.0)
email_validator (2.2.4)
activemodel
erb (6.0.1)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
faker (3.5.3)
i18n (>= 1.8.11, < 2)
faraday (2.14.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.5.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
net-http (~> 0.5)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
Expand All @@ -203,6 +216,8 @@ GEM
globalid (1.3.0)
activesupport (>= 6.1)
hashdiff (1.2.1)
hashie (5.1.0)
logger
i18n (1.14.8)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
Expand All @@ -222,6 +237,13 @@ GEM
activesupport (>= 7.0.0)
jmespath (1.6.2)
json (2.18.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
aes_key_wrap
base64
bindata
faraday (~> 2.0)
faraday-follow_redirects
jwt (3.1.2)
base64
kamal (2.10.1)
Expand Down Expand Up @@ -274,6 +296,8 @@ GEM
mocha (3.0.1)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
net-http (0.9.1)
uri (>= 0.11.1)
net-http-persistent (4.0.8)
connection_pool (>= 2.2.4, < 4)
net-imap (0.6.2)
Expand Down Expand Up @@ -307,6 +331,30 @@ GEM
racc (~> 1.4)
nokogiri (1.19.0-x86_64-linux-musl)
racc (~> 1.4)
omniauth (2.1.4)
hashie (>= 3.4.6)
logger
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth_openid_connect (0.8.0)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
openid_connect (2.3.1)
activemodel
attr_required (>= 1.0.0)
email_validator
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
mail
rack-oauth2 (~> 2.2)
swd (~> 2.0)
tzinfo
validate_url
webfinger (~> 2.0)
openssl (4.0.0)
ostruct (0.6.3)
parallel (1.27.0)
Expand Down Expand Up @@ -335,6 +383,17 @@ GEM
rack (3.2.4)
rack-mini-profiler (4.0.1)
rack (>= 1.2.0)
rack-oauth2 (2.3.0)
activesupport
attr_required
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (4.2.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
Expand Down Expand Up @@ -440,6 +499,11 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
thor (1.5.0)
thruster (0.1.17)
thruster (0.1.17-aarch64-linux)
Expand All @@ -458,6 +522,9 @@ GEM
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uri (1.1.1)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
vcr (6.4.0)
web-console (4.2.1)
actionview (>= 6.0.0)
Expand All @@ -467,6 +534,10 @@ GEM
web-push (3.1.0)
jwt (~> 3.0)
openssl (>= 3.0)
webfinger (2.1.3)
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
Expand Down Expand Up @@ -514,6 +585,8 @@ DEPENDENCIES
mittens
mocha
net-http-persistent
omniauth-rails_csrf_protection (~> 1.0)
omniauth_openid_connect (~> 0.8.0)
platform_agent
propshaft
puma (>= 5.0)
Expand Down
16 changes: 15 additions & 1 deletion app/controllers/concerns/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Authentication

etag { Current.identity.id if authenticated? }

include Authentication::ViaMagicLink, LoginHelper
include Authentication::ViaMagicLink, Authentication::ViaOidc, LoginHelper
end

class_methods do
Expand Down Expand Up @@ -104,4 +104,18 @@ def terminate_session
def session_token
cookies[:session_token]
end

def authentication_failed(message: "Something went wrong. Please try again.", redirect_path: new_session_path)
respond_to do |format|
format.html { redirect_to redirect_path, alert: message }
format.json { render json: { message: message }, status: :unauthorized }
end
end

def rate_limit_exceeded(message: "Try again later.", redirect_path: new_session_path)
respond_to do |format|
format.html { redirect_to redirect_path, alert: message }
format.json { render json: { message: message }, status: :too_many_requests }
end
end
end
15 changes: 15 additions & 0 deletions app/controllers/concerns/authentication/via_oidc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Authentication::ViaOidc
extend ActiveSupport::Concern

private
def authenticate_with_oidc(auth_hash)
identity = Identity.find_or_create_from_oidc(auth_hash)

if identity.present?
start_new_session_for identity
redirect_to after_authentication_url
else
authentication_failed(message: "Something went wrong using your identity provider.")
end
end
end
13 changes: 2 additions & 11 deletions app/controllers/sessions/magic_links_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,7 @@ def sign_in(magic_link)

def email_address_mismatch
clear_pending_authentication_token
alert_message = "Something went wrong. Please try again."

respond_to do |format|
format.html { redirect_to new_session_path, alert: alert_message }
format.json { render json: { message: alert_message }, status: :unauthorized }
end
authentication_failed
end

def invalid_code
Expand All @@ -76,10 +71,6 @@ def after_sign_in_url(magic_link)
end

def rate_limit_exceeded
rate_limit_exceeded_message = "Try again in 15 minutes."
respond_to do |format|
format.html { redirect_to session_magic_link_path, alert: rate_limit_exceeded_message }
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
end
super(message: "Try again in 15 minutes.", redirect_path: session_magic_link_path)
end
end
27 changes: 27 additions & 0 deletions app/controllers/sessions/oidc_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Sessions::OidcController < ApplicationController
disallow_account_scope
require_unauthenticated_access
rate_limit to: 10, within: 15.minutes, only: :create, with: :rate_limit_exceeded
skip_forgery_protection only: :create

layout "public"

def create
auth_hash = request.env["omniauth.auth"]

if auth_hash.present?
authenticate_with_oidc(auth_hash)
else
Rails.logger.debug "OIDC data not found"
authentication_failed(message: "OIDC authentication failed.")
end
rescue => e
Rails.error.report(e, severity: :error)
authentication_failed(message: "Error during OIDC authentication.")
end

def failure
error_type = params[:message] || "unknown_error"
authentication_failed(message: "OIDC authentication failed: #{error_type}")
end
end
9 changes: 0 additions & 9 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,6 @@ def email_address
params.expect(:email_address)
end

def rate_limit_exceeded
rate_limit_exceeded_message = "Try again later."

respond_to do |format|
format.html { redirect_to new_session_path, alert: rate_limit_exceeded_message }
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
end
end

def sign_in(identity)
redirect_to_session_magic_link identity.send_magic_link
end
Expand Down
8 changes: 8 additions & 0 deletions app/helpers/login_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ def redirect_to_login_url
def redirect_to_logout_url
redirect_to logout_url, allow_other_host: true
end

def oidc_enabled?
ENV["OIDC_ISSUER"].present?
end

def oidc_required?
ENV["OIDC_REQUIRED"] == "true"
end
end
3 changes: 2 additions & 1 deletion app/models/identity.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Identity < ApplicationRecord
include Joinable, Transferable
include Joinable, OidcCompatible, Transferable

has_many :access_tokens, dependent: :destroy
has_many :magic_links, dependent: :destroy
Expand All @@ -12,6 +12,7 @@ class Identity < ApplicationRecord
before_destroy :deactivate_users, prepend: true

validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :oidc_subject, uniqueness: { scope: :oidc_provider }, allow_nil: true
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }

def self.find_by_permissable_access_token(token, method:)
Expand Down
42 changes: 42 additions & 0 deletions app/models/identity/oidc_compatible.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Identity::OidcCompatible
extend ActiveSupport::Concern

class_methods do
def find_or_create_from_oidc(auth_hash)
provider = auth_hash.provider
subject = auth_hash.uid
email = auth_hash.info.email
email_verified = auth_hash.extra.raw_info.email_verified || false

return nil unless subject.present? && email.present?

# First, try to find existing identity by OIDC subject
identity = find_by(oidc_subject: subject, oidc_provider: provider)

if identity
if email_verified && identity.email_address != email
identity.update(email_address: email)
end
return identity
end

# Next, try to find by email and link OIDC credentials
identity = find_by(email_address: email)

if identity
identity.update(
oidc_subject: subject,
oidc_provider: provider
)
return identity
end

# Create new identity with OIDC
create(
email_address: email,
oidc_subject: subject,
oidc_provider: provider
)
end
end
end
Loading