diff options
| author | mo khan <mo@mokhan.ca> | 2025-03-17 21:38:32 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-03-17 21:38:32 -0600 |
| commit | a255f9818c4fab5c2e1c3e9ded61d37991b2cebb (patch) | |
| tree | 91fb5be83ec57fdddb6515d69de01fec36f078ea | |
| parent | 66edae4510b51570e6e4a21ef38dfc7defb63982 (diff) | |
feat: add a token introspection endpoint
| -rwxr-xr-x | bin/idp | 47 | ||||
| -rw-r--r-- | test/e2e_test.go | 13 |
2 files changed, 48 insertions, 12 deletions
@@ -105,7 +105,7 @@ module Authn end def create_access_token - ::Authz::JWT.new(sub: to_global_id.to_s, iat: Time.now.to_i) + ::Authz::JWT.new(sub: to_global_id.to_s) end def assertion_attributes_for(request) @@ -323,10 +323,30 @@ module Authz 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) - @claims = claims + now = Time.now.to_i + @claims = { + iat: now, + nbf: now, + jti: SecureRandom.uuid, + }.merge(claims) + end + + def active? + # TODO:: check if current time is within valid range + true end def to_jwt @@ -360,21 +380,14 @@ module Authz false end - def subject_of(token) - _header, claims, _signature = from_jwt(token) - claims[:sub] + 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 - - # TODO:: validate signature - def from_jwt(token) - token - .split('.', 3) - .map { |x| JSON.parse(Base64.strict_decode64(x), symbolize_names: true) rescue {} } - end end end @@ -499,6 +512,9 @@ module Authz 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) @@ -515,6 +531,13 @@ module Authz 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 <!DOCTYPE html> diff --git a/test/e2e_test.go b/test/e2e_test.go index c88f5fc6..40ed9439 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -277,6 +277,19 @@ func TestAuthx(t *testing.T) { project := x.Must(serde.FromJSON[map[string]string](response.Body)) assert.Equal(t, "foo", project["name"]) }) + + t.Run("token can be introspected", func(t *testing.T) { + client := conf.Client(ctx, credentials) + + io := bytes.NewBuffer(nil) + assert.NoError(t, serde.ToJSON(io, map[string]string{"token": credentials.AccessToken})) + response := x.Must(client.Post("http://idp.example.com:8080/oauth/introspect", "application/json", io)) + require.Equal(t, http.StatusOK, response.StatusCode) + + claims := x.Must(serde.FromJSON[map[string]interface{}](response.Body)) + assert.Equal(t, true, claims["active"]) + assert.Equal(t, "gid://example/Authn::User/1", claims["sub"]) + }) }) }) } |
