Add OIDC section for sign in and sign up page

- name: :oidc → Identifier for this login provider in the app.
- scope: [:openid, :email, :profile] → Tells the provider we want the user’s ID (openid), their email, and basic profile info (name, picture, etc.).
- response_type: :code → Uses Authorization Code Flow, which is more secure because tokens are not exposed in the URL.
- issuer: Rails.application.secrets.oidc_issuer → The base URL of the OIDC provider (e.g., Auth0). Used to find its config.
- discovery: true → Automatically fetches the provider’s endpoints from its discovery document instead of manually setting them.
- client_auth_method: :basic → Sends client ID and secret using HTTP Basic Auth when exchanging the code for tokens.

Add system tests for OIDC Auth

Edit the oauth docs to support OIDC auth
This commit is contained in:
Anamika Aggarwal
2025-08-07 05:31:13 +00:00
committed by Javi Martín
parent eab5f52e19
commit 5e263baed2
17 changed files with 390 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ describe Devise::OmniauthFormComponent do
Setting["feature.google_login"] = false
Setting["feature.wordpress_login"] = false
Setting["feature.saml_login"] = false
Setting["feature.oidc_login"] = false
end
it "is not rendered when all authentications are disabled" do
@@ -62,5 +63,14 @@ describe Devise::OmniauthFormComponent do
expect(page).to have_button "SAML"
expect(page).to have_button count: 1
end
it "renders the OIDC link when the feature is enabled" do
Setting["feature.oidc_login"] = true
render_inline component
expect(page).to have_button "OIDC"
expect(page).to have_button count: 1
end
end
end

View File

@@ -84,4 +84,96 @@ describe OmniauthTenantSetup do
end
end
end
describe "#oidc" do
it "uses different secrets for different tenants" do
create(:tenant, schema: "mars")
create(:tenant, schema: "venus")
stub_secrets(
oidc_client_id: "default-client-id",
oidc_client_secret: "default-client-secret",
oidc_issuer: "https://default-oidc.example.com",
oidc_redirect_uri: "https://default.consul.dev/auth/oidc/callback",
tenants: {
mars: {
oidc_client_id: "mars-client-id",
oidc_client_secret: "mars-client-secret",
oidc_issuer: "https://mars-oidc.example.com",
oidc_redirect_uri: "https://mars.consul.dev/auth/oidc/callback"
},
venus: {
oidc_client_id: "venus-client-id",
oidc_client_secret: "venus-client-secret",
oidc_issuer: "https://venus-oidc.example.com",
oidc_redirect_uri: "https://venus.consul.dev/auth/oidc/callback"
}
}
)
Tenant.switch("mars") do
mars_env = {
"omniauth.strategy" => double(options: {}),
"HTTP_HOST" => "mars.consul.dev"
}
OmniauthTenantSetup.oidc(mars_env)
mars_strategy_options = mars_env["omniauth.strategy"].options
expect(mars_strategy_options[:client_id]).to eq "mars-client-id"
expect(mars_strategy_options[:client_secret]).to eq "mars-client-secret"
expect(mars_strategy_options[:issuer]).to eq "https://mars-oidc.example.com"
expect(mars_strategy_options[:redirect_uri]).to eq "https://mars.consul.dev/auth/oidc/callback"
end
Tenant.switch("venus") do
venus_env = {
"omniauth.strategy" => double(options: {}),
"HTTP_HOST" => "venus.consul.dev"
}
OmniauthTenantSetup.oidc(venus_env)
venus_strategy_options = venus_env["omniauth.strategy"].options
expect(venus_strategy_options[:client_id]).to eq "venus-client-id"
expect(venus_strategy_options[:client_secret]).to eq "venus-client-secret"
expect(venus_strategy_options[:issuer]).to eq "https://venus-oidc.example.com"
expect(venus_strategy_options[:redirect_uri]).to eq "https://venus.consul.dev/auth/oidc/callback"
end
end
it "uses default secrets for non-overridden tenant" do
create(:tenant, schema: "earth")
stub_secrets(
oidc_client_id: "default-client-id",
oidc_client_secret: "default-client-secret",
oidc_issuer: "https://default-oidc.example.com",
oidc_redirect_uri: "https://default.consul.dev/auth/oidc/callback",
tenants: {
mars: {
oidc_client_id: "mars-client-id",
oidc_client_secret: "mars-client-secret",
oidc_issuer: "https://mars-oidc.example.com",
oidc_redirect_uri: "https://mars.consul.dev/auth/oidc/callback"
}
}
)
Tenant.switch("earth") do
earth_env = {
"omniauth.strategy" => double(options: {}),
"HTTP_HOST" => "earth.consul.dev"
}
OmniauthTenantSetup.oidc(earth_env)
earth_strategy_options = earth_env["omniauth.strategy"].options
expect(earth_strategy_options[:client_id]).to eq "default-client-id"
expect(earth_strategy_options[:client_secret]).to eq "default-client-secret"
expect(earth_strategy_options[:issuer]).to eq "https://default-oidc.example.com"
expect(earth_strategy_options[:redirect_uri]).to eq "https://default.consul.dev/auth/oidc/callback"
end
end
end
end

View File

@@ -645,6 +645,204 @@ describe "Users" do
end
end
end
context "OIDC" do
before { Setting["feature.oidc_login"] = true }
let(:oidc_hash_unverified_email) do
{
provider: "oidc",
uid: "oidc-user-123",
info: {
name: "oidctester",
email: "tester@consul.dev"
}
}
end
let(:oidc_hash_with_verified_email) do
{
provider: "oidc",
uid: "oidc-user-123",
info: {
name: "oidctester",
email: "tester@consul.dev",
verified: "1"
}
}
end
scenario "Sign up with a confirmed email from OIDC provider" do
OmniAuth.config.add_mock(:oidc, oidc_hash_with_verified_email)
visit new_user_registration_path
click_button "Sign up with OIDC"
expect(page).to have_content "Successfully identified as Oidc"
expect_to_be_signed_in
within("#notice") { click_button "Close" }
click_link "My account"
expect(page).to have_field "Username", with: "oidctester"
click_link "Change my login details"
expect(page).to have_field "Email", with: "tester@consul.dev"
end
scenario "Sign up with an unconfirmed email from OIDC provider" do
OmniAuth.config.add_mock(:oidc, oidc_hash_unverified_email)
visit new_user_registration_path
click_button "Sign up with OIDC"
expect(page).to have_content "To continue, please click on the confirmation " \
"link that we have sent you via email"
confirm_email
expect(page).to have_content "Your account has been confirmed"
expect(page).to have_current_path new_user_session_path
click_button "Sign in with OIDC"
expect(page).to have_content "Successfully identified as Oidc"
expect_to_be_signed_in
within("#notice") { click_button "Close" }
click_link "My account"
expect(page).to have_field "Username", with: "oidctester"
click_link "Change my login details"
expect(page).to have_field "Email", with: "tester@consul.dev"
end
scenario "Sign in with a user with an OIDC identity" do
user = create(:user, username: "oidctester", email: "tester@consul.dev", password: "My123456")
create(:identity, uid: "oidc-user-123", provider: "oidc", user: user)
OmniAuth.config.add_mock(:oidc, { provider: "oidc", uid: "oidc-user-123" })
visit new_user_session_path
click_button "Sign in with OIDC"
expect(page).to have_content "Successfully identified as Oidc"
expect_to_be_signed_in
within("#notice") { click_button "Close" }
click_link "My account"
expect(page).to have_field "Username", with: "oidctester"
click_link "Change my login details"
expect(page).to have_field "Email", with: "tester@consul.dev"
end
scenario "Sign in with a user without an OIDC identity keeps the username" do
create(:user, username: "tester", email: "tester@consul.dev", password: "My123456")
OmniAuth.config.add_mock(:oidc, oidc_hash_with_verified_email)
visit new_user_session_path
click_button "Sign in with OIDC"
expect(page).to have_content "Successfully identified as Oidc"
expect_to_be_signed_in
within("#notice") { click_button "Close" }
click_link "My account"
expect(page).to have_field "Username", with: "tester"
click_link "Change my login details"
expect(page).to have_field "Email", with: "tester@consul.dev"
end
scenario "OIDC user from one tenant cannot sign in to another tenant", :seed_tenants do
%w[mars venus].each do |schema|
create(:tenant, schema: schema)
Tenant.switch(schema) { Setting["feature.oidc_login"] = true }
end
Tenant.switch("mars") do
mars_user = create(:user, username: "marsuser", email: "mars@consul.dev")
create(:identity, uid: "mars-oidc-123", provider: "oidc", user: mars_user)
end
mars_oidc_hash = {
provider: "oidc",
uid: "mars-oidc-123",
info: {
name: "marsuser",
email: "mars@consul.dev"
}
}
OmniAuth.config.add_mock(:oidc, mars_oidc_hash)
with_subdomain("mars") do
visit new_user_session_path
click_button "Sign in with OIDC"
expect(page).to have_content "Successfully identified as Oidc"
within("#notice") { click_button "Close" }
click_link "My account"
expect(page).to have_field "Username", with: "marsuser"
end
with_subdomain("venus") do
visit new_user_session_path
click_button "Sign in with OIDC"
expect(page).to have_content "To continue, please click on the confirmation " \
"link that we have sent you via email"
end
end
scenario "Allows user authentication when OIDC token has expired" do
user = create(:user, username: "oidctester", email: "tester@consul.dev")
create(:identity, uid: "oidc-user-123", provider: "oidc", user: user)
expired_oidc_hash = oidc_hash_with_verified_email.merge(
credentials: {
token: "expired_token",
expires_at: 1.hour.ago.to_i,
expires: true
}
)
OmniAuth.config.add_mock(:oidc, expired_oidc_hash)
visit new_user_session_path
click_button "Sign in with OIDC"
expect(page).to have_content "Successfully identified as Oidc"
expect_to_be_signed_in
end
scenario "Handle missing email claim from OIDC provider" do
oidc_hash_no_email = {
provider: "oidc",
uid: "oidc-user-no-email",
info: {
name: "noemailuser"
}
}
OmniAuth.config.add_mock(:oidc, oidc_hash_no_email)
visit new_user_registration_path
click_button "Sign up with OIDC"
expect(page).to have_content "1 error prevented this Account from being saved."
expect(page).to have_content "can't be blank"
end
end
end
scenario "Sign out" do