Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OIDC config primitives #49

Merged
merged 66 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
e7d31cb
rebased
Oct 7, 2024
593126b
class vars
Oct 8, 2024
ad3c237
cleanup
Oct 8, 2024
770bc42
cleanup
Oct 8, 2024
8e999f3
adding comments
Oct 8, 2024
3e9da0e
comments
Oct 8, 2024
616e979
fixed yml file
Oct 8, 2024
1903d11
cleanup
Oct 8, 2024
806fca2
cleanup client configure
Oct 8, 2024
84b1638
comments
Oct 8, 2024
9d55d26
cleanup
Oct 9, 2024
6915b0e
fix provider addr
Oct 9, 2024
9ff6d1f
separate out issuer config
Oct 9, 2024
97fdf62
added comments
Oct 9, 2024
20cb10d
cleanup readme
Oct 9, 2024
8f6d97a
cleanup
Oct 9, 2024
e496ef6
fix for prod
Oct 10, 2024
d33a69c
fixed issuer
Oct 10, 2024
14326f0
cleanup
Oct 10, 2024
4957347
comment
Oct 10, 2024
f3a4828
cleanup local names
Oct 10, 2024
3d3d1fb
cleanup local names
Oct 10, 2024
5c59530
review fixes
Oct 11, 2024
52e2420
application.rb fixes
Oct 11, 2024
0617b30
application.rb fixes
Oct 11, 2024
71a2259
cleanup configs
Oct 11, 2024
289dea6
added initial_user()
Oct 11, 2024
87a56aa
don't refresh the provider unnecessarily
Oct 11, 2024
66c9bb1
cleanup
Oct 11, 2024
0d6bf1c
rubocop
Oct 11, 2024
bb90ac4
memoized oidc_provider
Oct 11, 2024
a5dfa94
cleanup
Oct 11, 2024
3897835
removed unneeded reader policy
Oct 11, 2024
9a49426
moved provider to test dir
Oct 11, 2024
9bf5124
init
Oct 15, 2024
a733396
moved provider
Oct 15, 2024
2a90392
refactored init code
Oct 15, 2024
56b6f76
rake task
Oct 16, 2024
52122d0
rake task
Oct 16, 2024
2793425
added Config
Oct 16, 2024
609cbcf
rake task working
Oct 16, 2024
a43272f
moved oidc comment block
Oct 16, 2024
0772fdd
removed utils/oidc.rb
Oct 16, 2024
3402488
cleanup
Oct 16, 2024
a474cbd
added tests
Oct 16, 2024
4f7d5d3
fixed token
Oct 16, 2024
2aba8fb
provider tests
Oct 16, 2024
7456b02
cleanup
Oct 17, 2024
3774510
fix test comments
Oct 17, 2024
caeb603
commented rake task
Oct 17, 2024
ed18d74
fixed initial user
Oct 17, 2024
bbb3255
cleanup review comments
Oct 17, 2024
5c5f3c5
rubocop
Oct 17, 2024
ca256c8
Merge branch 'main' into addOidcProviderRebase
GeorgeJahad Oct 17, 2024
70329c3
Because the "VAULT_SSL_CERT" env var is set, added ssl
Oct 18, 2024
de43ef3
updated Brakeman
Oct 18, 2024
8f195ca
added oidc provider ssl
Oct 18, 2024
a87e951
fixed up provider certs
Oct 18, 2024
6b866af
fixed oidc_provider for ssl
Oct 20, 2024
7ae52ba
fix for oidc_provider/ssl
Oct 21, 2024
5e0eda0
fixed issuer path
Oct 21, 2024
350d286
add comment for oidcProvider tls
Oct 21, 2024
dc4ece1
fixed issuer
Oct 21, 2024
879b0db
fixed comment
Oct 21, 2024
2f35777
rubocop
Oct 21, 2024
909516e
fixed readme
Oct 21, 2024
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
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
"forwardPorts": [3000, 5432, 8200],
"forwardPorts": [3000, 5432, 8200, 8300],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "bundle install && rake db:setup",
"postCreateCommand": "bundle install && rake oidc_provider:configure && rake db:setup",

// Configure tool-specific properties.
// "customizations": {},
Expand Down
9 changes: 9 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ services:
VAULT_DEV_ROOT_TOKEN_ID: root_token
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200

oidc_provider:
image: hashicorp/vault:latest
restart: unless-stopped
ports:
- 8300:8300
environment:
VAULT_DEV_ROOT_TOKEN_ID: root_token
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8300

app_registry:
image: node:latest
restart: unless-stopped
Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,74 @@ docker build -t astral:latest .
```
docker run -p 3000:3000 astral:latest
```

# OIDC configuration
The OIDC modules allow the assignment of a policy to an OIDC user, by
mapping that user's email address to a policy we create. They work as
follows:

OidcProvider::configure_as_oidc_provider() creates an OIDC provider
and user on a separate dedicated vault instance. The user created has
a username/password/email addr, that can be accessed with OIDC auth
from vault.
Copy link
Collaborator

Choose a reason for hiding this comment

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

a little confusing since there are two vaults, maybe "that can accessed with OIDC auth in the principal Vault instance" ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


Clients::Vault::Oidc::configure_as_oidc_client() creates an OIDC
Copy link
Collaborator

Choose a reason for hiding this comment

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

this will be accessed as Clients::Vault.configure_as_oidc_client since the module is included there

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed

client on our vault instance. It connects to that provider just
created. When a user tries to auth, the client connects to the
provider, which opens up a browser window allowing the user to enter
his username/password.

On success, the provider returns an OIDC token, which includes the
user's email addr.
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe don't abbreviate "addr" in the doc?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


The OIDC client has been configured to map that email address to an
entity in vault, which has the policy which we want the user to have.

So the mapping goes from the email address on the provider, to the
policy in vault.

Note that this provider is mainly meant to be used in our dev/test
environment to excercise the client. In a prod env, a real OIDC
provider would probably be used instead, (by configuring it in
config/astral.yml).

# Logging into vault with OIDC

The rails test's configure the OIDC provider, so if the tests pass,
you can invoke the oidc login as follows:

```
export VAULT_ADDR=http://127.0.0.1:8200; vault login -method=oidc
```

You should do this on your host machine, not in docker. This will
allow a browser window to open on your host. When it does, select
"username" option with user test/test. (That is the username/pw
configured by the rails tests.)
Copy link
Collaborator

Choose a reason for hiding this comment

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

configured at start up

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


When that succeeds, you should see something like the following in the cli:
```
Success! You are now authenticated
.
identity_policies ["[email protected]"]
.
.
```

Note that "identity_policies" includes "[email protected]", which is the
policy we created for this user.

To make this work smoothly with the browser, you should add the
following to the /etc/hosts file on your host:

```
127.0.0.1 oidc_provider
```

Finally, if you restart the docker vault container, it will recreate
the provider settings, so you will need to clear the browser's
"oidc_provider" cookie. Otherwise you will see this error:

```
* Vault login failed. Expired or missing OAuth state.
```
1 change: 1 addition & 0 deletions app/lib/clients/vault.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Vault
extend Clients::Vault::Policy
extend Clients::Vault::Entity
extend Clients::Vault::EntityAlias
extend Clients::Vault::Oidc

class_attribute :token

Expand Down
48 changes: 48 additions & 0 deletions app/lib/clients/vault/oidc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module Clients
class Vault
module Oidc
def configure_as_oidc_client(issuer, client_id, client_secret)
create_client_config(issuer, client_id, client_secret)
create_default_role(client_id)
end

def configure_oidc_user(name, email, policy)
client.sys.put_policy(email, policy)
put_entity(name, email)
put_entity_alias(name, email, "oidc")
end

def get_oidc_client_config
client.logical.read("auth/oidc/config")
end
private

def create_client_config(issuer, client_id, client_secret)
if client_id.nil? || !oidc_auth_data.nil?
return
end
client.logical.write("/sys/auth/oidc", type: "oidc")
client.logical.write("auth/oidc/config",
oidc_discovery_url: issuer,
oidc_client_id: client_id,
oidc_client_secret: client_secret,
default_role: "default")
end

def create_default_role(client_id)
client.logical.write(
"auth/oidc/role/default",
bound_audiences: client_id,
allowed_redirect_uris: Config[:oidc_redirect_uris],
user_claim: "email",
oidc_scopes: "email",
token_policies: "default")
end

def oidc_auth_data
auth_list = client.logical.read("/sys/auth")
auth_list.data[:"oidc/"]
end
end
end
end
18 changes: 18 additions & 0 deletions app/lib/clients/vault/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ def create_astral_policy
path "#{kv_mount}/data/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "identity/entity" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "identity/entity/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "identity/entity-alias" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "/sys/auth" {
capabilities = ["read"]
}
path "auth/oidc/config" {
capabilities = ["read"]
}
path "/sys/policy/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
HCL

client.sys.put_policy("astral_policy", policy)
Expand Down
86 changes: 86 additions & 0 deletions app/lib/utils/oidc_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
class OidcProvider
attr_reader :client_id
attr_reader :client_secret
attr_reader :provider

def configure
provider = oidc_provider.logical.read("identity/oidc/provider/astral")
if provider.nil?
create_provider_webapp
create_provider_with_email_scope
create_entity_for_initial_user
create_userpass_for_initial_user
map_userpass_to_entity
else
get_client_info
end
end


def get_client_info
app = oidc_provider.logical.read(WEBAPP_NAME)
@client_id = app.data[:client_id]
@client_secret = app.data[:client_secret]
[ @client_id, @client_secret ]
end

def get_info
oidc_provider.logical.read("identity/oidc/provider/astral")
end

private
WEBAPP_NAME = "identity/oidc/client/astral"

def oidc_provider
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think naming this vault_client might make downstream usage a little clearer? since the class is OidcProvider (abstraction) but rubber is meeting road here (vault specifically is the provider).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@provider ||=
::Vault::Client.new(
address: Config[:oidc_provider_addr],
token: Config[:vault_token]
)
end

def create_provider_webapp
oidc_provider.logical.write(
WEBAPP_NAME,
redirect_uris: Config[:oidc_redirect_uris],
assignments: "allow_all")
get_client_info
end

def create_provider_with_email_scope
oidc_provider.logical.write("identity/oidc/scope/email",
template: '{"email": {{identity.entity.metadata.email}}}')
oidc_provider.logical.write("identity/oidc/provider/astral",
issuer: "http://oidc_provider:8300",
allowed_client_ids: @client_id,
scopes_supported: "email")
oidc_provider.logical.read("identity/oidc/provider/astral")
end

def create_entity_for_initial_user
oidc_provider.logical.write("identity/entity",
policies: "default",
name: Config[:initial_user_name],
metadata: "email=#{Config[:initial_user_email]}",
disabled: false)
end

def create_userpass_for_initial_user
oidc_provider.logical.delete("/sys/auth/userpass")
oidc_provider.logical.write("/sys/auth/userpass", type: "userpass")
oidc_provider.logical.write("/auth/userpass/users/#{Config[:initial_user_name]}",
password: Config[:initial_user_password])
end

def map_userpass_to_entity
entity = oidc_provider.logical.read(
"identity/entity/name/#{Config[:initial_user_name]}")
entity_id = entity.data[:id]
auth_list = oidc_provider.logical.read("/sys/auth")
accessor = auth_list.data[:"userpass/"][:accessor]
oidc_provider.logical.write("identity/entity-alias",
name: Config[:initial_user_name],
canonical_id: entity_id,
mount_accessor: accessor)
end
end
8 changes: 8 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ class Application < Rails::Application
Clients::Vault.token = Config[:vault_token]
Clients::Vault.configure_kv
Clients::Vault.configure_pki
get_oidc_config
Clients::Vault.configure_as_oidc_client(config.astral.oidc_issuer,
config.astral.oidc_client_id,
config.astral.oidc_client_secret)
Clients::Vault.rotate_token
end

def get_oidc_config
# do nothing by default
end
end
end
15 changes: 15 additions & 0 deletions config/astral.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,25 @@ shared:

audit_log_file: <%= "#{Rails.root.join('log')}/astral-audit.log" %>

oidc_issuer:
oidc_client_id:
oidc_client_secret:
oidc_redirect_uris: http://localhost:8250/oidc/callback

test:
oidc_issuer: http://oidc_provider:8300/v1/identity/oidc/provider/astral
oidc_provider_addr: http://oidc_provider:8300
initial_user_name: test
initial_user_password: test
initial_user_email: [email protected]
Copy link
Collaborator

Choose a reason for hiding this comment

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

you could eliminate one repetition of this config by putting these dev/test values in shared and the blank overrides in production

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

cert_ttl: <%= 24.hours.in_seconds %>

development:
oidc_issuer: http://oidc_provider:8300/v1/identity/oidc/provider/astral
oidc_provider_addr: http://oidc_provider:8300
initial_user_name: test
initial_user_password: test
initial_user_email: [email protected]

production:
vault_create_root: false
5 changes: 5 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "active_support/core_ext/integer/time"
require_relative "../../app/lib/utils/oidc_provider"

Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
Expand Down Expand Up @@ -72,4 +73,8 @@

# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
# config.generators.apply_rubocop_autocorrect_after_generate!

def get_oidc_config
Copy link
Collaborator

Choose a reason for hiding this comment

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

similar, if this is the default impl and noop is the production case, you reduce one repetition

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

config.astral.oidc_client_id, config.astral.oidc_client_secret = OidcProvider.new.get_client_info
end
end
5 changes: 5 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "active_support/core_ext/integer/time"
require_relative "../../app/lib/utils/oidc_provider"

# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
Expand Down Expand Up @@ -64,4 +65,8 @@

# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true

def get_oidc_config
config.astral.oidc_client_id, config.astral.oidc_client_secret = OidcProvider.new.get_client_info
end
end
12 changes: 12 additions & 0 deletions lib/tasks/oidc_provider.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require "rake"
require_relative "../../app/lib/utils/oidc_provider"
require_relative "../../app/lib/config"

# Rake tasks for oidc provider
namespace :oidc_provider do
desc "Configure the provider"
task :configure do
OidcProvider.new.configure
puts "oidc provider configured"
end
end
19 changes: 19 additions & 0 deletions test/lib/clients/oidc_provider_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "test_helper"

class OidcProviderTest < ActiveSupport::TestCase
setup do
@provider = OidcProvider.new
end

test ".get_info returns correct info" do
info = @provider.get_info
assert_equal Config[:oidc_issuer], info.data[:issuer]
assert_equal "email", info.data[:scopes_supported][0]
end

test ".get_client_info return correct info" do
info = @provider.get_client_info
assert_equal Config[:oidc_client_id], info[0]
assert_equal Config[:oidc_client_secret], info[1]
end
end
Loading