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:
committed by
Javi Martín
parent
eab5f52e19
commit
5e263baed2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user