#!/usr/bin/env ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "declarative_policy", "~> 1.0" gem "erb", "~> 4.0" gem "globalid", "~> 1.0" gem "google-protobuf", "~> 3.0" gem "rack", "~> 3.0" gem "rackup", "~> 2.0" gem "saml-kit", "~> 1.0" gem "twirp", "~> 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' $scheme = ENV.fetch("SCHEME", "http") $port = ENV.fetch("PORT", 8282).to_i $host = ENV.fetch("HOST", "localhost:#{$port}") class JWT attr_reader :claims def initialize(claims) @claims = claims end def to_jwt [ Base64.strict_encode64(JSON.generate({alg: "RS256", typ: "JWT"})), Base64.strict_encode64(JSON.generate(claims)), Base64.strict_encode64(JSON.generate({})), ].join(".") end end class User def initialize(attributes) @attributes = attributes end def name_id_for(name_id_format) if name_id_format == Saml::Kit::Namespaces::EMAIL_ADDRESS @attributes[:email] else @attributes[:id] end end def assertion_attributes_for(request) { custom: 'custom attribute', email: @attributes[:email], access_token: JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt, } 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 Saml::Kit.configure do |x| x.entity_id = "#{$scheme}://#{$host}/saml/metadata.xml" x.registry = OnDemandRegistry.new x.logger = Logger.new("/dev/stderr") end class IdentityProvider def call(env) path = env['PATH_INFO'] case env['REQUEST_METHOD'] when 'GET' case path when '/.well-known/openid-configuration' return openid_metadata when '/.well-known/oauth-authorization-server' return oauth_metadata when '/.well-known/webfinger' # RFC-7033 return not_found when "/saml/metadata.xml" return saml_metadata when "/saml/new" # TODO:: render a login page return saml_post_back(Rack::Request.new(env)) when "/oauth/authorize" # RFC-6749 return get_authorize(Rack::Request.new(env)) else return not_found end when 'POST' case path when "/saml/new" return saml_post_back(Rack::Request.new(env)) when "/oauth/authorize" # RFC-6749 return post_authorize(Rack::Request.new(env)) when "/oauth/token" # RFC-6749 return [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({ access_token: JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt, token_type: "Bearer", expires_in: 3600, refresh_token: SecureRandom.hex(32) })]] when "/oauth/revoke" # RFC-7009 return not_found else return not_found end end not_found end private def saml_metadata xml = Saml::Kit::Metadata.build_xml do |builder| builder.contact_email = 'hi@example.com' builder.organization_name = "Acme, Inc" builder.organization_url = "https://example.com" 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::EMAIL_ADDRESS] x.attributes << :Username end end [200, { 'Content-Type' => "application/samlmetadata+xml" }, [xml]] 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 def get_authorize(request) template = <<~ERB