Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
891e2f0
feat: support RefreshToken
yuzhou721 Jul 16, 2025
9d84b44
feat: support response type idToken
yuzhou721 Jul 17, 2025
ca29195
fix: clear log
yuzhou721 Jul 17, 2025
dd2f1d8
feat: support RefreshToken
yuzhou721 Jul 23, 2025
3a04d75
feat: add cors
yuzhou721 Jul 28, 2025
b293c69
feat: add cors
yuzhou721 Jul 28, 2025
4b8cf65
fix: cors
yuzhou721 Jul 28, 2025
a2cb86b
fix: cors
yuzhou721 Jul 28, 2025
b6c7e59
fix: cors
yuzhou721 Jul 28, 2025
f14be0a
fix: add openid_connect
yuzhou721 Jul 29, 2025
b81e359
fix: cors
yuzhou721 Jul 29, 2025
9013f59
feat: add authorization confirm page
yuzhou721 Jul 29, 2025
643bd96
feat: add authorization confirm page
yuzhou721 Jul 29, 2025
0f5a903
fix: no method
yuzhou721 Jul 29, 2025
ef1a4dc
feat: add idToken field
yuzhou721 Jul 29, 2025
78aa6d8
feat: add idToken field
yuzhou721 Jul 29, 2025
530a3db
feat: add idToken field
yuzhou721 Jul 29, 2025
de69c33
fix: idToken field
yuzhou721 Jul 29, 2025
cd80ae9
feat: jwks endpoint add field
yuzhou721 Jul 29, 2025
ee79665
feat: Access Token add scope
yuzhou721 Aug 2, 2025
3bc0374
feat: Access Token add scope
yuzhou721 Aug 2, 2025
60ce5d8
feat: Access Token add scope
yuzhou721 Aug 2, 2025
64f2012
feat: add nonce to userInfo claims
yuzhou721 Aug 2, 2025
14d8618
feat: add nonce to userInfo claims
yuzhou721 Aug 2, 2025
6ba932a
feat: add nonce to userInfo claims
yuzhou721 Aug 2, 2025
7ae9bb4
feat: add nonce to userInfo claims
yuzhou721 Aug 2, 2025
f94b515
feat: add nonce to userInfo claims
yuzhou721 Aug 2, 2025
6e6fef8
feat: add nonce to userInfo claims
yuzhou721 Aug 2, 2025
097b1b8
fix: refresh_token error
yuzhou721 Aug 3, 2025
d690e21
fix: refresh_token error
yuzhou721 Aug 3, 2025
6636c14
fix: refresh_token error
yuzhou721 Aug 3, 2025
ab8375c
fix: refresh_token error
yuzhou721 Aug 3, 2025
069092f
fix: refresh_token error
yuzhou721 Aug 3, 2025
0bbd2c8
feat: include user claims in id token
yuzhou721 Aug 6, 2025
38b6836
feat: include user claims in id token
yuzhou721 Aug 6, 2025
bf01e51
feat: include user claims in id token
yuzhou721 Aug 6, 2025
7574368
rever
yuzhou721 Aug 7, 2025
811ddac
rever
yuzhou721 Aug 7, 2025
174fe38
remove rack-cors gem
yuzhou721 Aug 8, 2025
59ac78a
feat: Support multiple domains
yuzhou721 Aug 8, 2025
7232735
feat: Support multiple domains
yuzhou721 Aug 8, 2025
a12ccb0
feat: Support multiple domains
yuzhou721 Aug 14, 2025
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 @@ -13,3 +13,5 @@ gemspec

# To use a debugger
# gem 'byebug', group: [:development, :test]

gem "openid_connect"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is specified by the gemspec

30 changes: 22 additions & 8 deletions app/controllers/oidc_provider/authorizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module OIDCProvider
class AuthorizationsController < ApplicationController
include Concerns::ConnectEndpoint

skip_before_action :verify_authenticity_token

before_action :require_oauth_request
before_action :require_response_type_code
before_action :require_client
Expand All @@ -15,13 +17,11 @@ def create

authorization = build_authorization_with(requested_scopes)

oauth_response.code = authorization.code
oauth_response.code = authorization.code if @requested_type==:code or @requested_type == :hybrid
oauth_response.id_token = authorization.id_token.to_jwt if @requested_type==:id_token or @requested_type == :hybrid
oauth_response.redirect_uri = @redirect_uri
oauth_response.approve!
redirect_to oauth_response.location, allow_other_host: true

# If we ever need to support denied authorizations that is done by:
# oauth_request.access_denied!
redirect_to oauth_response.location,allow_other_host: true
end

private
Expand All @@ -31,7 +31,8 @@ def build_authorization_with(scopes)
client_id: @client.identifier,
nonce: oauth_request.nonce,
scopes: scopes,
account: oidc_current_account
account: oidc_current_account,
issuer: request.base_url

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to keep this as the configured issuer rather that being request specific

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other IDPs such as Authentik implement it this way as well, for adapting to multi-domain scenarios

)
end

Expand All @@ -46,9 +47,22 @@ def requested_scopes
helper_method :requested_scopes

def require_response_type_code
return if oauth_request.response_type == :code
type = oauth_request.response_type
type.nil? && oauth_request.unsupported_response_type!
case type
when :code
@requested_type=:code
when :id_token
@requested_type=:id_token
when ->(ary) do ary.include?(:code) && ary.include?(:id_token) end
@requested_type=:hybrid
# when type.include?("token")
# @response_type=:token
else
oauth_request.unsupported_response_type!
end
Comment on lines +50 to +63

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we turn this whole method into a single case statement?



oauth_request.unsupported_response_type!
end

def reset_login_if_necessary
Expand Down
21 changes: 11 additions & 10 deletions app/controllers/oidc_provider/discovery_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,24 @@ def webfinger_discovery
end

def openid_configuration
issuer = request.base_url
config = OpenIDConnect::Discovery::Provider::Config::Response.new(
issuer: OIDCProvider.issuer,
authorization_endpoint: authorizations_url(host: OIDCProvider.issuer),
token_endpoint: tokens_url(host: OIDCProvider.issuer),
userinfo_endpoint: user_info_url(host: OIDCProvider.issuer),
end_session_endpoint: end_session_url(host: OIDCProvider.issuer),
jwks_uri: jwks_url(host: OIDCProvider.issuer),
issuer: issuer,
authorization_endpoint: authorizations_url(host: issuer),
token_endpoint: tokens_url(host: issuer),
userinfo_endpoint: user_info_url(host: issuer),
end_session_endpoint: end_session_url(host: issuer),
jwks_uri: jwks_url(host: issuer),
scopes_supported: ["openid"] + OIDCProvider.supported_scopes.map(&:name),
response_types_supported: [:code],
grant_types_supported: [:authorization_code],
response_types_supported: [:code, :id_token],
grant_types_supported: [:authorization_code, :refresh_token],
subject_types_supported: [:public],
id_token_signing_alg_values_supported: [:RS256],
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'],
claims_supported: ['sub', 'iss', 'name', 'email']
claims_supported: ['sub', 'iss', 'name', 'email','aud','email_verified','family_name','given_name']
)
render json: config
end
end

end
end
3 changes: 2 additions & 1 deletion app/controllers/oidc_provider/user_infos_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ class UserInfosController < ApplicationController
before_action :require_access_token

def show
render json: AccountToUserInfo.new.(current_token.authorization.account, current_token.authorization.scopes)

render json: AccountToUserInfo.new.(current_token.authorization.account, current_token.authorization.scopes, current_token.authorization.nonce)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to return the nonce in the user info request

Suggested change
render json: AccountToUserInfo.new.(current_token.authorization.account, current_token.authorization.scopes, current_token.authorization.nonce)
render json: AccountToUserInfo.new.(current_token.authorization.account, current_token.authorization.scopes)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When integrating with ABM (apple business manage), nonce is required

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find that in the spec. I'd prefer to remain spec compliant, but I would be willing to introduce some quirks provided they are documented

end
end
end
34 changes: 30 additions & 4 deletions app/models/oidc_provider/access_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,37 @@ class AccessToken < ApplicationRecord
attribute :token, :string, default: -> { SecureRandom.hex 32 }
attribute :expires_at, :datetime, default: -> { 1.hours.from_now }

def to_bearer_token
Rack::OAuth2::AccessToken::Bearer.new(
access_token: token,
expires_in: (expires_at - Time.now).to_i
def to_bearer_token(with_refresh_token)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword argument for clarity please

Suggested change
def to_bearer_token(with_refresh_token)
def to_bearer_token(with_refresh_token:)

if with_refresh_token
Rack::OAuth2::AccessToken::Bearer.new(
access_token: token,
expires_in: (expires_at - Time.now).to_i,
refresh_token: (get_refresh_token(authorization.client_id,authorization.scopes)||generate_refresh_token(authorization.client_id,authorization.scopes)).token,
scope: authorization.scopes
)
else
Rack::OAuth2::AccessToken::Bearer.new(
access_token: token,
expires_in: (expires_at - Time.now).to_i,
scope: authorization.scopes
)
end
Comment on lines +11 to +24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if with_refresh_token
Rack::OAuth2::AccessToken::Bearer.new(
access_token: token,
expires_in: (expires_at - Time.now).to_i,
refresh_token: (get_refresh_token(authorization.client_id,authorization.scopes)||generate_refresh_token(authorization.client_id,authorization.scopes)).token,
scope: authorization.scopes
)
else
Rack::OAuth2::AccessToken::Bearer.new(
access_token: token,
expires_in: (expires_at - Time.now).to_i,
scope: authorization.scopes
)
end
bearer_token_opts = {
access_token: token,
expires_in: (expires_at - Time.now).to_i,
scope: authorization.scopes
}
if with_refresh_token
refresh_token = get_refresh_token(authorization.client_id,authorization.scopes) || generate_refresh_token(authorization.client_id,authorization.scopes)
bearer_token_opts[:refresh_token] = refresh_token.token
end
Rack::OAuth2::AccessToken::Bearer.new(**bearer_token_opts)
end

end

private
def get_refresh_token(client_id,scopes)
RefreshToken
.valid
.where(client_id: client_id, revoked_at: nil,scopes:scopes)
.first
Comment on lines +29 to +32

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not scoped to a specific authorization

end
def generate_refresh_token(client_id,scopes)
RefreshToken.create!(
client_id: client_id,
scopes: scopes,
authorization: authorization
)
end

end
end
8 changes: 7 additions & 1 deletion app/models/oidc_provider/authorization.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
module OIDCProvider
class Authorization < ApplicationRecord
belongs_to :account, class_name: OIDCProvider.account_class
has_one :access_token
has_one :access_token, dependent: :destroy
has_one :id_token

scope :valid, -> { where(arel_table[:expires_at].gteq(Time.now.utc)) }

attribute :code, :string, default: -> { SecureRandom.hex 32 }
attribute :expires_at, :datetime, default: -> { 5.minutes.from_now }
attribute :issuer, :string

serialize :scopes, coder: JSON

Expand All @@ -20,6 +21,11 @@ def access_token
super || expire! && generate_access_token!
end

def refresh!
access_token = create_access_token!
access_token.save!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The access token will already be saved

end

def id_token
super || generate_id_token!
end
Expand Down
27 changes: 21 additions & 6 deletions app/models/oidc_provider/id_token.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true
require 'openid_connect/response_object/id_token/id_token_with_user_info'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not require files here, let's do it in the engine


module OIDCProvider
class IdToken < ApplicationRecord
Expand All @@ -11,14 +12,29 @@ class IdToken < ApplicationRecord
delegate :account, to: :authorization

def to_response_object
OpenIDConnect::ResponseObject::IdToken.new(
iss: OIDCProvider.issuer,
base_claims ={
iss: authorization.issuer,
sub: account.send(OIDCProvider.account_identifier),
aud: authorization.client_id,
nonce: nonce,
exp: expires_at.to_i,
iat: created_at.to_i
)
iat: created_at.to_i,
auth_time:created_at.to_i,
amr: ['pwd']
}
if OIDCProvider.include_user_claims_in_id_token
extra_claims = {
email: account.send(OIDCProvider.account_email),
email_verified: true,
given_name: account.send(OIDCProvider.account_given_name),
family_name: account.send(OIDCProvider.account_family_name),
}
Comment on lines +26 to +31

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardcodes these specific claims, but we could have introduced other claims through scopes

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see how to implement this feature using scopes

OpenIDConnect::ResponseObject::IdTokenWithUserInfo.new(**extra_claims, **base_claims)
else
OpenIDConnect::ResponseObject::IdToken.new(
**base_claims
)
end
end

def to_jwt
Expand All @@ -30,7 +46,6 @@ def to_jwt
class << self
def config
{
issuer: OIDCProvider.issuer,
jwk_set: JSON::JWK::Set.new(public_jwk)
}
end
Expand All @@ -48,7 +63,7 @@ def private_jwk
end

def public_jwk
JSON::JWK.new key_pair.public_key
JSON::JWK.new key_pair.public_key, {use: 'sig',alg: 'RS256'}
end
end
end
Expand Down
12 changes: 12 additions & 0 deletions app/models/oidc_provider/refresh_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module OIDCProvider
class RefreshToken < ApplicationRecord
scope :valid, -> { where(arel_table[:expires_at].gteq(Time.now.utc)) }
belongs_to :authorization
attribute :token, :string, default: -> { SecureRandom.hex 32 }
attribute :expires_at, :datetime, default: -> { 1.month.from_now }
attribute :revoked_at, :datetime
serialize :scopes, coder: JSON
end
end
15 changes: 15 additions & 0 deletions app/views/layouts/application.html.erb

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Views are out of scope here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is not excluded. Previously, an authorization page was added here to handle the issue of POST requests not redirecting

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next commit will delete this file

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>ConnectOp</title>
<%= csrf_meta_tags %>
<%= action_cable_meta_tag %>

<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>

<body>
<%= yield %>
</body>
</html>
15 changes: 15 additions & 0 deletions app/views/oidc_provider/authorizations/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<article>
<h2>Authorization Request by <%= @client.name %></h2>
<%= form_tag authorizations_path do %>
<ul>
<% requested_scopes.each do |scope| %>
<li><%= scope %></li>
<% end %>
</ul>
<% [:client_id, :response_type, :redirect_uri, :scope, :state, :nonce].each do |key| %>
<%= hidden_field_tag key, oauth_request.send(key) %>
<% end %>
<p><%= submit_tag :deny %></p>
<p><%= submit_tag :approve %></p>
<% end %>
</article>
17 changes: 17 additions & 0 deletions db/migrate/20250714123456_create_oidc_provider_refresh_tokens.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

class CreateOIDCProviderRefreshTokens < ActiveRecord::Migration[5.1]
def change
create_table :oidc_provider_refresh_tokens do |t|
t.string :client_id, null: false
t.belongs_to :authorization, null: false

t.string :token, null: false
t.datetime :expires_at, null: false
t.datetime :revoked_at
t.text :scopes, null: false

t.timestamps
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddIssuerToOIDCProviderAuthorizations < ActiveRecord::Migration[5.1]
def change
add_column :oidc_provider_authorizations, :issuer, :string
end

end
1 change: 0 additions & 1 deletion lib/generators/oidc_provider/templates/initializer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
OIDCProvider.configure do |config|
config.issuer = "https://myoidcprovider.org"
# config.account_class = "Administrator"

config.add_client do
Expand Down
15 changes: 15 additions & 0 deletions lib/oidc_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Scopes
Profile = "profile"
Email = "email"
Address = "address"
Phone = "phone"
OfflineAccess = "offline_access"
end

autoload :TokenEndpoint, 'oidc_provider/token_endpoint'
Expand Down Expand Up @@ -42,6 +44,19 @@ module Scopes
mattr_accessor :account_identifier
@@account_identifier = :id

mattr_accessor :account_email
@@account_identifier = :email

mattr_accessor :account_given_name
@@account_identifier = :given_name

mattr_accessor :account_family_name
@@account_identifier = :family_name

# Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.
mattr_accessor :include_user_claims_in_id_token
@@include_user_claims_in_id_token = false
Comment on lines +47 to +58

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use scopes to define this info


mattr_accessor :after_sign_out_path

def self.add_client(&block)
Expand Down
5 changes: 3 additions & 2 deletions lib/oidc_provider/account_to_user_info.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
require 'openid_connect/response_object/user_info/user_info_with_nonce'
module OIDCProvider
class AccountToUserInfo
def call(account, scope_names)
def call(account, scope_names, nonce)
scopes = scope_names.map { |name| OIDCProvider.supported_scopes.detect { |scope| scope.name == name } }.compact
OpenIDConnect::ResponseObject::UserInfo.new(sub: account.send(OIDCProvider.account_identifier)).tap do |user_info|
OpenIDConnect::ResponseObject::UserInfoWithNonce.new({sub: account.send(OIDCProvider.account_identifier), nonce: nonce}).tap do |user_info|
scopes.each do |scope|
UserInfoBuilder.new(user_info, account).run(&scope.work)
end
Expand Down
Loading