From c583bcd1473205104a1e1af812ed4976d30c7baa Mon Sep 17 00:00:00 2001 From: mo khan Date: Fri, 2 May 2025 14:29:41 -0600 Subject: refactor: remove anything unrelated to the authz daemon --- bin/idp | 895 ---------------------------------------------------------------- 1 file changed, 895 deletions(-) delete mode 100755 bin/idp (limited to 'bin/idp') diff --git a/bin/idp b/bin/idp deleted file mode 100755 index 62462deb..00000000 --- a/bin/idp +++ /dev/null @@ -1,895 +0,0 @@ -#!/usr/bin/env ruby - -require "bundler/inline" - -gemfile do - source "https://rubygems.org" - - gem "base64", "~> 0.1" - gem "bcrypt", "~> 3.0" - gem "csv", "~> 3.0" - gem "declarative_policy", "~> 1.0" - gem "erb", "~> 4.0" - gem "globalid", "~> 1.0" - gem "google-protobuf", "~> 3.0" - gem "rack", "~> 3.0" - gem "rack-session", "~> 2.0" - gem "rackup", "~> 2.0" - gem "saml-kit", "1.4.0" - gem "twirp", "~> 1.0" - gem "warden", "~> 1.0" - gem "webrick", "~> 1.0" -end - -lib_path = Pathname.new(__FILE__).parent.parent.join('lib').realpath.to_s -$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path) - -require 'authx/rpc' - -$scheme = ENV.fetch("SCHEME", "http") -$port = ENV.fetch("PORT", 8282).to_i -$host = ENV.fetch("HOST", "localhost:#{$port}") - -DeclarativePolicy.configure do - name_transformation do |name| - "::Authz::#{name}Policy" - end -end - -Warden::Manager.serialize_into_session do |user| - user.id -end - -Warden::Manager.serialize_from_session do |id| - ::Authn::User.find(id) -end - -Warden::Strategies.add(:password) do - def valid? - params['username'] && params['password'] - end - - def authenticate! - user = ::Authn::User.login(params.transform_keys(&:to_sym)) - user.nil? ? fail!("Could not log in") : success!(user) - end -end - -module HTTPHelpers - def current_user?(request) - request.env['warden'].authenticated? - end - - def current_user(request) - request.env['warden'].user - end - - def default_headers - { - 'X-Powered-By' => 'IdP' - } - end - - def http_not_found - [404, default_headers, []] - end - - def http_ok(headers = {}, body = nil) - [200, default_headers.merge(headers), [body]] - end - - def http_redirect_to(location) - [302, { 'Location' => "#{$scheme}://#{$host}#{location}" }, []] - end -end - -module Authn - class User - include ::BCrypt - - class << self - def all - @all ||= ::CSV.read(File.join(__dir__, "../db/idp/users.csv"), headers: true).map do |row| - new(row.to_h.transform_keys(&:to_sym)) - end - end - - def find(id) - all.find do |user| - user[:id] == id - end - end - - def find_by - all.find do |user| - yield user - end - end - - def find_by_email(email) - find_by do |user| - user[:email] == email - end - end - - def find_by_username(username) - find_by do |user| - user[:username] == username - end - end - - def login(params = {}) - user = find_by_username(params[:username]) - user&.valid_password?(params[:password]) ? user : nil - end - end - - attr_reader :id - - def initialize(attributes) - @attributes = attributes - @id = self[:id] - end - - def [](attribute) - @attributes.fetch(attribute.to_sym) - end - - def name_id_for(name_id_format) - if name_id_format == Saml::Kit::Namespaces::EMAIL_ADDRESS - self[:email] - else - self[:id] - end - end - - def create_access_token - ::Authz::JWT.new( - sub: to_global_id.to_s, - auth_time: Time.now.to_i, - email: self[:email], - username: self[:username], - ) - end - - def create_id_token - ::Authz::JWT.new(sub: to_global_id.to_s) - end - - def assertion_attributes_for(request) - { - email: self[:email], - } - end - - def valid_password?(entered_password) - ::BCrypt::Password.new(self[:password_digest]) == entered_password - end - - def to_global_id - ::GlobalID.new( - ::URI::GID.build( - app: "example", - model_name: "User", - model_id: id, - params: {} - ) - ).to_s - end - end - - class OnDemandRegistry < Saml::Kit::DefaultRegistry - def metadata_for(entity_id) - found = super(entity_id) - return found if found - - register_url(entity_id, verify_ssl: false) - super(entity_id) - end - end - - class SessionsController - include ::HTTPHelpers - - def call(env) - request = Rack::Request.new(env) - case request.request_method - when Rack::GET - case request.path - when '/sessions/new' - return get_login(request) - end - when Rack::POST - case request.path - when '/sessions' - if (user = env['warden'].authenticate(:password)) - path = request.params["redirect_back"] ? request.params["redirect_back"] : "/" - return http_redirect_to(path) - else - return http_redirect_to("/sessions/new") - end - when '/sessions/delete' - request.env['warden'].logout - return http_redirect_to('/') - end - end - - http_not_found - end - - private - - def get_login(request) - template = <<~ERB - - - - IdP - - - - - - -
- - -
-
- - -
- - " /> - -
-
- - - ERB - html = ERB.new(template, trim_mode: '-').result(binding) - [200, { 'Content-Type' => "text/html" }, [html]] - end - end - - class SAMLController - include ::HTTPHelpers - - def initialize(scheme, host) - Saml::Kit.configure do |x| - x.entity_id = "#{$scheme}://#{$host}/saml/metadata.xml" - x.registry = OnDemandRegistry.new - x.logger = Logger.new("/dev/stderr") - end - - @saml_metadata = Saml::Kit::Metadata.build do |builder| - builder.contact_email = 'hi@example.com' - builder.organization_name = "Acme, Inc" - builder.organization_url = "#{scheme}://#{host}" - builder.build_identity_provider do |x| - x.add_single_sign_on_service("#{scheme}://#{host}/saml/new", binding: :http_post) - x.name_id_formats = [Saml::Kit::Namespaces::PERSISTENT, Saml::Kit::Namespaces::EMAIL_ADDRESS] - x.attributes << :email - end - end - end - - def call(env) - request = Rack::Request.new(env) - case request.request_method - when Rack::GET - case request.path - when "/saml/continue" - if current_user?(request) - saml_params = request.session[:saml_params] - return saml_post_back(request, current_user(request), saml_params) - else - return http_redirect_to("/sessions/new?redirect_back=/saml/continue") - end - when "/saml/metadata.xml" - return http_ok( - { 'Content-Type' => "application/samlmetadata+xml" }, - saml_metadata.to_xml(pretty: true) - ) - end - when Rack::POST - case request.path - when "/saml/new" - saml_params = saml_params_from(request) - - if current_user?(request) - return saml_post_back(request, current_user(request), saml_params) - else - request.session[:saml_params] = saml_params - return http_redirect_to("/sessions/new?redirect_back=/saml/continue") - end - end - end - - http_not_found - end - - private - - attr_reader :saml_metadata - - def saml_post_back(request, user, saml_params) - saml_request = binding_for(request).deserialize(saml_params) - - @builder = nil - url, saml_params = saml_request.response_for( - user, - binding: :http_post, - relay_state: saml_params[:RelayState] - ) { |builder| @builder = builder } - template = <<~ERB - - - - IdP - - - - - - -
- - -

Recieved SAML Request

- - -

Sending SAML Response (IdP -> SP)

- -
- <%- saml_params.each do |(key, value)| -%> - - <%- end -%> - -
-
- - - ERB - erb = ERB.new(template, trim_mode: '-') - html = erb.result(binding) - [200, { 'Content-Type' => "text/html" }, [html]] - end - - def saml_params_from(request) - if request.post? - { - "SAMLRequest" => request.params["SAMLRequest"], - "RelayState" => request.params["RelayState"], - } - else - query_string = request.query_string - on = query_string.include?("&") ? "&" : "&" - Hash[query_string.split(on).map { |x| x.split("=", 2) }].symbolize_keys - end - end - - def binding_for(request) - Saml::Kit::Bindings::HttpPost.new(location: "#{$scheme}://#{$host}/saml/new") - end - end -end - -class Organization - class << self - def find(id) - new - end - end -end - -module Authz - class OrganizationPolicy < DeclarativePolicy::Base - condition(:owner) { true } - - rule { owner }.enable :read_project - rule { owner }.enable :read_group - rule { owner }.enable :create_project - end - - class JWT - class << self - # TODO:: validate signature - def decode(encoded) - _header, body, _signature = encoded - .split('.', 3) - .map { |x| JSON.parse(Base64.strict_decode64(x), symbolize_names: true) rescue {} } - new(body) - end - end - - attr_reader :claims - - def initialize(claims) - now = Time.now.to_i - @claims = { - iss: "#{$scheme}://#{$host}", - iat: now, - aud: "", - nbf: now, - jti: SecureRandom.uuid, - exp: now + 3600, - }.merge(claims) - end - - def [](claim) - claims.fetch(claim) - end - - def active? - now = Time.now.to_i - self[:nbf] <= now && now < self[:exp] - end - - def to_jwt - [ - Base64.strict_encode64(JSON.generate(alg: "none")), - Base64.strict_encode64(JSON.generate(claims)), - "" - ].join(".") - end - end - - module Rpc - class Ability - def allowed(request, env) - { - result: can?(request) - } - end - - private - - def can?(request) - subject = subject_of(request.subject) - resource = resource_from(request.resource) - permission = request.permission.to_sym - - policy = DeclarativePolicy.policy_for(subject, resource) - policy.can?(permission) - rescue StandardError => error - puts error.inspect - false - end - - def subject_of(encoded_token) - token = ::Authz::JWT.decode(encoded_token) - token&.claims[:sub] - end - - def resource_from(global_id) - GlobalID::Locator.locate(global_id) - end - end - end - - class AuthorizationGrant - class << self - def all - @all ||= [] - end - - def find_by(params) - case params[:grant_type] - when 'authorization_code' - authorization_code_grant(params[:code], params[:code_verifier]) - when 'refresh_token' - refresh_grant(params[:refresh_token]) - when 'client_credentials' - client_credentials_grant(params) - when 'password' - password_grant(params[:username], params[:password]) - when "urn:ietf:params:oauth:grant-type:saml2-bearer" # RFC-7522 - saml_assertion_grant(params[:assertion]) - when 'urn:ietf:params:oauth:grant-type:jwt-bearer' # RFC7523 - jwt_bearer_grant(params) - end - end - - # TODO:: implement `code_verifier` param - def authorization_code_grant(code, code_verifier) - all.find do |grant| - grant.active? && grant.code == code - end - end - - def refresh_grant(refresh_token) - raise NotImplementedError - end - - def client_credential_grant(params) - raise NotImplementedError - end - - def password_grant(username, password) - raise NotImplementedError - end - - def saml_assertion_grant(encoded_saml_assertion) - xml = Base64.decode64(encoded_saml_assertion) - # TODO:: Validate signature and prevent assertion reuse - saml_assertion = Saml::Kit::Document.to_saml_document(xml) - - user = ::Authn::User.find_by_email(saml_assertion.name_id) || - ::Authn::User.find(saml_assertion.name_id) - new(user, saml_assertion: saml_assertion) - end - - def jwt_bearer_grant(params) - raise NotImplementedError - end - - def create!(user, params = {}) - new(user, params).tap do |grant| - all << grant - end - end - end - - attr_reader :code, :user, :params - - def initialize(user, params = {}) - @user = user - @params = params - @code = SecureRandom.uuid - @exchanged_at = nil - end - - def active? - @exchanged_at.nil? - end - - def inactive? - !active? - end - - def create_access_token - raise "Invalid code" unless active? - - user.create_access_token.tap do - @exchanged_at = Time.now - end - end - - def exchange - { - access_token: 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) - }.tap do |body| - if params["scope"]&.include?("openid") - body[:id_token] = user.create_id_token.to_jwt - end - end - rescue StandardError => error - { - error: error.message, - error_description: error.backtrace, - } - end - end - - class OAuthController - include ::HTTPHelpers - - def call(env) - request = Rack::Request.new(env) - - case request.request_method - when Rack::GET - 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(oauth_params) - else - 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 - when "/oauth/authorize" # RFC-6749 - return post_authorize(request) - when "/oauth/introspect" # RFC-7662 - params = request.content_type == "application/json" ? JSON.parse(request.body.read, symbolize_names: true) : Hash[URI.decode_www_form(request.body.read)].transform_keys(&:to_sym) - return post_introspect(params.slice(:token, :token_type_hint)) - when "/oauth/token" # RFC-6749 - params = request.content_type == "application/json" ? JSON.parse(request.body.read, symbolize_names: true) : Hash[URI.decode_www_form(request.body.read)].transform_keys(&:to_sym) - grant = AuthorizationGrant.find_by(params) - - return [404, { "Content-Type" => "application/json" }, [JSON.pretty_generate(error: 404, error_description: "Not Found")]] if grant.nil? || grant.inactive? - return [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(grant.exchange)]] - 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 - end - end - http_not_found - end - - private - - def post_introspect(params) - token = ::Authz::JWT.decode(params[:token]) - return [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(token.claims.merge(active: token.active?))]] - end - - def get_authorize(oauth_params) - template = <<~ERB - - - - IdP - - - - - - -
- - -

Authorize?

-

Client ID: <%= oauth_params['client_id'] %>

-
- - - - - - - - - -
-
- - - ERB - html = ERB.new(template, trim_mode: '-').result(binding) - [200, { 'Content-Type' => "text/html" }, [html]] - end - - def post_authorize(request) - params = request.params.slice('client_id', 'redirect_uri', 'response_type', 'response_mode', 'state', 'code_challenge_method', 'code_challenge', 'scope') - grant = AuthorizationGrant.create!(current_user(request), params) - case params['response_type'] - when 'code' - case params['response_mode'] - when 'fragment' - return [302, { 'Location' => "#{params['redirect_uri']}#code=#{grant.code}&state=#{params['state']}" }, []] - when 'query' - return [302, { 'Location' => "#{params['redirect_uri']}?code=#{grant.code}&state=#{params['state']}" }, []] - else - # TODO:: form post - end - when 'token' - return http_not_found - else - return http_not_found - end - end - end -end - -class IdentityProvider - include ::HTTPHelpers - - def call(env) - request = Rack::Request.new(env) - - case request.request_method - when Rack::GET - case request.path - when '/' - if current_user?(request) - return get_dashboard(request) - else - return http_redirect_to("/sessions/new") - end - when '/.well-known/openid-configuration' - return openid_metadata - when '/.well-known/oauth-authorization-server' - return oauth_metadata - when '/.well-known/webfinger' # RFC-7033 - return http_not_found - else - return http_not_found - end - end - http_not_found - end - - private - - def get_dashboard(request) - template = <<~ERB - - - - IdP - - - - - - -
- - -

Hello, <%= current_user(request)[:username] %>

-
- - - ERB - erb = ERB.new(template, trim_mode: '-') - html = erb.result(binding) - [200, { 'Content-Type' => "text/html" }, [html]] - end - - # GET /.well-known/oauth-authorization-server - def oauth_metadata - [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({ - issuer: "#{$scheme}://#{$host}/.well-known/oauth-authorization-server", - authorization_endpoint: "#{$scheme}://#{$host}/oauth/authorize", - token_endpoint: "#{$scheme}://#{$host}/oauth/token", - jwks_uri: "", # RFC-7517 - registration_endpoint: "", # RFC-7591 - scopes_supported: ["openid", "profile", "email"], - response_types_supported: ["code", "code id_token", "id_token", "token id_token"], - response_modes_supported: ["query", "fragment", "form_post"], - grant_types_supported: ["authorization_code", "implicit"], # RFC-7591 - token_endpoint_auth_methods_supported: ["client_secret_basic"], # RFC-7591 - token_endpoint_auth_signing_alg_values_supported: ["RS256"], - service_documentation: "", - ui_locales_supported: ["en-US"], - op_policy_uri: "", - op_tos_uri: "", - revocation_endpoint: "#{$scheme}://#{$host}/oauth/revoke", # RFC-7009 - revocation_endpoint_auth_methods_supported: ["client_secret_basic"], - revocation_endpoint_auth_signing_alg_values_supported: ["RS256"], - introspection_endpoint: "#{$scheme}://#{$host}/oauth/introspect", # RFC-7662 - introspection_endpoint_auth_methods_supported: ["client_secret_basic"], - introspection_endpoint_auth_signing_alg_values_supported: ["RS256"], - code_challenge_methods_supported: [], # RFC-7636 - })]] - end - - # GET /.well-known/openid-configuration - def openid_metadata - [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({ - issuer: "#{$scheme}://#{$host}/.well-known/oauth-authorization-server", - authorization_endpoint: "#{$scheme}://#{$host}/oauth/authorize", - token_endpoint: "#{$scheme}://#{$host}/oauth/token", - userinfo_endpoint: "#{$scheme}://#{$host}/oidc/user/", - jwks_uri: "", # RFC-7517 - registration_endpoint: nil, - scopes_supported: ["openid", "profile", "email"], - response_types_supported: ["code", "code id_token", "id_token", "token id_token"], - response_modes_supported: ["query", "fragment", "form_post"], - grant_types_supported: ["authorization_code", "implicit"], # RFC-7591 - acr_values_supported: [], - subject_types_supported: ["pairwise", "public"], - id_token_signing_alg_values_supported: ["RS256"], - id_token_encryption_alg_values_supported: [], - id_token_encryption_enc_values_supported: [], - userinfo_signing_alg_values_supported: ["RS256"], - userinfo_encryption_alg_values_supported: [], - userinfo_encryption_enc_values_supported: [], - request_object_signing_alg_values_supported: ["none", "RS256"], - request_object_encryption_alg_values_supported: [], - request_object_encryption_enc_values_supported: [], - token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic", "client_secret_jwt", "private_key_jwt"], - token_endpoint_auth_signing_alg_values_supported: [], - display_values_supported: [], - claim_types_supported: ["normal", "aggregated", "distributed"], - claims_supported: [ - "acr", - "auth_time", - "email", - "email_verified", - "family_name", - "given_name", - "iss", - "locale", - "name", - "nickname", - "picture", - "profile", - "sub", - "website" - ], - service_documentation: nil, - claims_locales_supported: [], - ui_locales_supported: ["en-US"], - claims_parameter_supported: false, - request_parameter_supported: false, - request_uri_paramater_supported: false, - require_request_uri_registration: false, - op_policy_uri: "", - op_tos_uri: "", - })]] - end -end - -if __FILE__ == $0 - app = Rack::Builder.new do - use Rack::CommonLogger - use Rack::Reloader - use Rack::Session::Cookie, { domain: $host.split(":", 2)[0], path: "/", secret: SecureRandom.hex(64) } - use ::Warden::Manager do |config| - config.default_scope = :user - config.default_strategies :password - end - - map "/twirp" do - # https://github.com/arthurnn/twirp-ruby/wiki/Service-Handlers - run ::Authx::Rpc::AbilityService.new(::Authz::Rpc::Ability.new) - end - map "/oauth" do - run ::Authz::OAuthController.new - end - - map "/saml" do - run Authn::SAMLController.new($scheme, $host) - end - - map "/sessions" do - run Authn::SessionsController.new - end - run IdentityProvider.new - end.to_app - - Rackup::Server.start(app: app, Port: $port) -end -- cgit v1.2.3