diff options
| author | mo khan <mo@mokhan.ca> | 2025-03-12 16:15:20 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-03-12 16:15:20 -0600 |
| commit | 9b267c499709472cd20d95df76b53fc6c571e797 (patch) | |
| tree | 695d20441792f97bdc374196c8f6d98ba89ca9a7 | |
| parent | f62507b993e42c1d3fc96b2cafdcac51259b7ab0 (diff) | |
feat: require a login before authorizing an auth grant
| -rwxr-xr-x | bin/idp | 53 | ||||
| -rwxr-xr-x | bin/ui | 31 | ||||
| -rw-r--r-- | test/e2e_test.go | 16 |
3 files changed, 71 insertions, 29 deletions
@@ -89,8 +89,11 @@ module Authn end end + attr_reader :id + def initialize(attributes) @attributes = attributes + @id = self[:id] end def [](attribute) @@ -106,7 +109,7 @@ module Authn end def create_access_token - ::Authz::JWT.new(sub: self[:id], iat: Time.now.to_i) + ::Authz::JWT.new(sub: to_global_id.to_s, iat: Time.now.to_i) end def assertion_attributes_for(request) @@ -118,6 +121,10 @@ module Authn def valid_password?(entered_password) ::BCrypt::Password.new(self[:password_digest]) == entered_password end + + def to_global_id + ::GlobalID.create(self, app: "example").to_s + end end class OnDemandRegistry < Saml::Kit::DefaultRegistry @@ -139,6 +146,7 @@ module Authn when Rack::GET case request.path when '/sessions/new' + request.session.delete(:user_id) return get_login(request) end when Rack::POST @@ -376,29 +384,38 @@ module Authz case request.request_method when Rack::GET - case request.path_info - when "/authorize" # RFC-6749 + case request.path + when "/oauth/authorize/continue" + if current_user?(request) + return get_authorize(request.session[:oauth_params]) + end + when "/oauth/authorize" # RFC-6749 + oauth_params = request.params.slice('client_id', 'scope', 'redirect_uri', 'response_mode', 'response_type', 'state', 'code_challenge_method', 'code_challenge') if current_user?(request) - return get_authorize(request) + return get_authorize(oauth_params) else - http_redirect_to("/saml/") + request.session[:oauth_params] = oauth_params + return http_redirect_to("/sessions/new?redirect_back=/oauth/authorize/continue") end else return http_not_found end when Rack::POST - case request.path_info - when "/authorize" # RFC-6749 + case request.path + when "/oauth/authorize" # RFC-6749 return post_authorize(request) - when "/token" # RFC-6749 + when "/oauth/token" # RFC-6749 + # TODO:: Look up authorization grant by (code, saml_assertion) + user = Authn::User.new(id: SecureRandom.uuid) return [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({ - access_token: ::Authz::JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt, + access_token: user.create_access_token.to_jwt, token_type: "Bearer", issued_token_type: "urn:ietf:params:oauth:token-type:access_token", expires_in: 3600, refresh_token: SecureRandom.hex(32) })]] when "/oauth/revoke" # RFC-7009 + # TODO:: Revoke the JWT token and make it ineligible for usage return http_not_found else return http_not_found @@ -407,7 +424,7 @@ module Authz http_not_found end - def get_authorize(request) + def get_authorize(oauth_params) template = <<~ERB <!DOCTYPE html> <html> @@ -415,14 +432,14 @@ module Authz <body> <h2>Authorize?</h2> <form id="authorize-form" action="/oauth/authorize" method="post"> - <input type="hidden" name="client_id" value="<%= request.params['client_id'] %>" /> - <input type="hidden" name="scope" value="<%= request.params['scope'] %>" /> - <input type="hidden" name="redirect_uri" value="<%= request.params['redirect_uri'] %>" /> - <input type="hidden" name="response_mode" value="<%= request.params['response_mode'] %>" /> - <input type="hidden" name="response_type" value="<%= request.params['response_type'] %>" /> - <input type="hidden" name="state" value="<%= request.params['state'] %>" /> - <input type="hidden" name="code_challenge_method" value="<%= request.params['code_challenge_method'] %>" /> - <input type="hidden" name="code_challenge" value="<%= request.params['code_challenge'] %>" /> + <input type="hidden" name="client_id" value="<%= oauth_params['client_id'] %>" /> + <input type="hidden" name="scope" value="<%= oauth_params['scope'] %>" /> + <input type="hidden" name="redirect_uri" value="<%= oauth_params['redirect_uri'] %>" /> + <input type="hidden" name="response_mode" value="<%= oauth_params['response_mode'] %>" /> + <input type="hidden" name="response_type" value="<%= oauth_params['response_type'] %>" /> + <input type="hidden" name="state" value="<%= oauth_params['state'] %>" /> + <input type="hidden" name="code_challenge_method" value="<%= oauth_params['code_challenge_method'] %>" /> + <input type="hidden" name="code_challenge" value="<%= oauth_params['code_challenge'] %>" /> <input id="submit-button" type="submit" value="Submit" /> </form> </body> @@ -20,6 +20,8 @@ $port = ENV.fetch("PORT", 8283).to_i $host = ENV.fetch("HOST", "localhost:#{$port}") $idp_host = ENV.fetch("IDP_HOST", "localhost:8282") +Net::Hippie.logger = Logger.new($stdout, level: :debug) + class OnDemandRegistry < Saml::Kit::DefaultRegistry def metadata_for(entity_id) found = super(entity_id) @@ -90,16 +92,25 @@ class UI end def oauth_callback(request) - response = Net::Hippie.default_client.post( - "http://#{$idp_host}/oauth/token", - headers: { 'Authorization' => Net::Hippie.basic_auth('client_id', 'secret') }, - body: { - grant_type: "authorization_code", - code: request.params['code'], - code_verifier: "not_implemented" - } - ) - [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(request.params.merge(JSON.parse(response.body)))]] + client = Net::Hippie::Client.new + response = client.with_retry do |x| + client.post( + "http://#{$idp_host}/oauth/token", + headers: { 'Authorization' => Net::Hippie.basic_auth('client_id', 'secret') }, + body: { + grant_type: "authorization_code", + code: request.params['code'], + code_verifier: "not_implemented" + } + ) + end + if response.code.to_i == 200 + [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate( + request.params.merge(JSON.parse(response.body)) + )]] + else + [response.code, response.header, [response.body]] + end end def saml_post_to_idp(request) diff --git a/test/e2e_test.go b/test/e2e_test.go index 12e28edd..b465d764 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -43,6 +43,7 @@ func TestAuthx(t *testing.T) { } t.Run("GET http://ui.example.com:8080/saml/new", func(t *testing.T) { + assert.NoError(t, page.Context().ClearCookies()) x.Must(page.Goto("http://ui.example.com:8080/saml/new")) action := x.Must(page.Locator("#idp-form").GetAttribute("action")) assert.Equal(t, "http://idp.example.com:8080/saml/new", action) @@ -61,8 +62,15 @@ func TestAuthx(t *testing.T) { t.Run("OIDC", func(t *testing.T) { t.Run("GET http://ui.example.com:8080/oidc/new", func(t *testing.T) { + assert.NoError(t, page.Context().ClearCookies()) x.Must(page.Goto("http://ui.example.com:8080/oidc/new")) - assert.Contains(t, page.URL(), "http://idp.example.com:8080/oauth/authorize") + + assert.Contains(t, page.URL(), "http://idp.example.com:8080/sessions/new") + page.Locator("#username").Fill("username1") + page.Locator("#password").Fill("password1") + assert.NoError(t, page.Locator("#login-button").Click()) + + assert.Contains(t, page.URL(), "http://idp.example.com:8080/oauth/authorize/continue") assert.NoError(t, page.Locator("#submit-button").Click()) assert.Contains(t, page.URL(), "http://ui.example.com:8080/oauth/callback") @@ -177,7 +185,13 @@ func TestAuthx(t *testing.T) { oauth2.SetAuthURLParam("response_type", "code"), oauth2.SetAuthURLParam("response_mode", "fragment"), ) + assert.NoError(t, page.Context().ClearCookies()) x.Must(page.Goto(authURL)) + + page.Locator("#username").Fill("username1") + page.Locator("#password").Fill("password1") + assert.NoError(t, page.Locator("#login-button").Click()) + assert.NoError(t, page.Locator("#submit-button").Click()) uri := x.Must(url.Parse(page.URL())) |
