diff --git a/spec/authly_spec.cr b/spec/authly_spec.cr index 793533b..8dc8eb8 100644 --- a/spec/authly_spec.cr +++ b/spec/authly_spec.cr @@ -24,7 +24,7 @@ describe Authly do code: code) if id_token = token.id_token - id_token_decoded = Authly.jwt_decode(id_token).first + id_token_decoded = Authly.decode_token(id_token).first token.should be_a Authly::AccessToken id_token_decoded["user_id"].should eq "username" @@ -122,7 +122,7 @@ describe Authly do describe ".introspect" do it "returns active token" do a_token = Authly::AccessToken.new(client_id, scope) - expected_token = Authly.jwt_decode(a_token.access_token).first + expected_token = Authly.decode_token(a_token.access_token).first token = Authly.introspect(a_token.access_token) token.should eq({ diff --git a/src/authly.cr b/src/authly.cr index 17531ec..8fe0ca9 100644 --- a/src/authly.cr +++ b/src/authly.cr @@ -33,52 +33,33 @@ module Authly Grant.new(grant_type, **args).token end - def self.jwt_encode(payload) - JWT.encode(payload, config.security.secret_key, config.security.algorithm) + def self.encode_token(payload) + token_manager = TokenManager.new + token_manager.encode(payload) end - def self.jwt_decode(token, secret_key = config.security.public_key) - JWT.decode token, secret_key, config.security.algorithm + def self.decode_token(token) + token_manager = TokenManager.new + token_manager.decode(token) end - def self.revoke(jti) - Authly.config.providers.jti_provider.revoke(jti) + def self.revoke(token) + token_manager = TokenManager.new + token_manager.revoke(token) end - def self.revoked?(jti) - Authly.config.providers.jti_provider.revoked?(jti) + def self.revoke?(token) + token_manager = TokenManager.new + token_manager.revoke?(token) end def self.valid?(token) - decoded_token, _header = jwt_decode(token) - jti = decoded_token["jti"].to_s - return false if revoked?(jti) - - exp = decoded_token["exp"].to_s.to_i - Time.utc.to_unix < exp - rescue e : JWT::DecodeError - Log.error { "Invalid token - #{e.message}" } - false + token_manager = TokenManager.new + token_manager.valid?(token) end def self.introspect(token : String) - # Decode the JWT, verify the signature and expiration - payload, _header = jwt_decode(token) - - # Check if the token is expired (exp claim is typically in seconds since epoch) - if Time.local.to_unix > payload["exp"].to_s.to_i - return {active: false, exp: payload["exp"]} - end - - # Return authly access token - { - active: true, - scope: payload["scope"], - cid: payload["cid"], - exp: payload["exp"], - sub: payload["sub"], - } - rescue JWT::DecodeError - {active: false} + token_manager = TokenManager.new + token_manager.introspect(token) end end diff --git a/src/authly/access_token.cr b/src/authly/access_token.cr index e3e0967..e2f7ebd 100644 --- a/src/authly/access_token.cr +++ b/src/authly/access_token.cr @@ -28,7 +28,7 @@ module Authly end private def generate_token - Authly.jwt_encode({ + Authly.encode_token({ "sub" => Random::Secure.hex(32), "iss" => Authly.config.issuer, "cid" => @client_id, @@ -40,7 +40,7 @@ module Authly end def refresh_token - Authly.jwt_encode({ + Authly.encode_token({ "sub" => @client_id, "name" => "refresh token", "iat" => Time.utc.to_unix, diff --git a/src/authly/code.cr b/src/authly/code.cr index 403b3b5..6c30b6a 100644 --- a/src/authly/code.cr +++ b/src/authly/code.cr @@ -22,7 +22,7 @@ module Authly end def jwt - Authly.jwt_encode({ + Authly.encode_token({ "code" => code, "challenge" => challenge, "method" => method, diff --git a/src/authly/configuration.cr b/src/authly/configuration.cr index 053c0f4..f825d4e 100644 --- a/src/authly/configuration.cr +++ b/src/authly/configuration.cr @@ -26,6 +26,7 @@ module Authly property security : SecurityConfiguration = SecurityConfiguration.new property ttl : TTLConfiguration = TTLConfiguration.new property providers : ProvidersConfiguration = ProvidersConfiguration.new + property token_type : String = "jwt" # Singleton instance @@instance : Configuration? diff --git a/src/authly/error.cr b/src/authly/error.cr index fecae9d..7eee29c 100644 --- a/src/authly/error.cr +++ b/src/authly/error.cr @@ -11,9 +11,14 @@ module Authly unauthorized_client: "This client is not authorized to use the requested grant type", unsupported_grant_type: "Invalid or unknown grant type", access_denied: "The user or authorization server denied the request", + unsupported_token_type: "The authorization server does not support the revocation of the presented token type", } class Error(Code) < Exception + def self.unsupported_token_type + raise Error(400).new(:unsupported_token_type) + end + def self.owner_credentials raise Error(400).new(:owner_credentials) end diff --git a/src/authly/grant.cr b/src/authly/grant.cr index 3ae9a9f..fb9c7c9 100644 --- a/src/authly/grant.cr +++ b/src/authly/grant.cr @@ -61,12 +61,12 @@ module Authly private def generate_id_token if scope.includes? "openid" - Authly.jwt_encode Authly.owners.id_token auth_code["user_id"].as_s + Authly.encode_token Authly.owners.id_token auth_code["user_id"].as_s end end private def auth_code - Authly.jwt_decode(@code).first + Authly.decode_token(@code).first end private def scope : String diff --git a/src/authly/grants/refresh_token.cr b/src/authly/grants/refresh_token.cr index 3f634f3..5f842ef 100644 --- a/src/authly/grants/refresh_token.cr +++ b/src/authly/grants/refresh_token.cr @@ -14,7 +14,7 @@ module Authly end private def validate_refresh_token! - Authly.jwt_decode(@refresh_token) + Authly.decode_token(@refresh_token) rescue e raise Error.invalid_grant end diff --git a/src/authly/token_manager.cr b/src/authly/token_manager.cr new file mode 100644 index 0000000..4961d0c --- /dev/null +++ b/src/authly/token_manager.cr @@ -0,0 +1,41 @@ +module Authly + # Token Management Class + class TokenManager + @token_provider : TokenProvider + + TOKEN_PROVIDER = { + "jwt" => JWTTokenProvider.new, + "opaque" => OpaqueTokenProvider.new + } + + def initialize + @token_provider = TOKEN_PROVIDER.fetch(Authly.config.token_type) do + raise Error.unsupported_token_type + end + end + + def encode(payload) : String + @token_provider.encode(payload) + end + + def decode(token : String) + @token_provider.decode(token) + end + + def valid?(token : String) : Bool + @token_provider.valid?(token) + end + + def revoke(token : String) + @token_provider.revoke(token) + end + + def revoke?(token : String) : Bool + @token_provider.revoke?(token) + end + + def introspect(token : String) + @token_provider.introspect(token) + end + end +end diff --git a/src/authly/token_provider.cr b/src/authly/token_provider.cr new file mode 100644 index 0000000..8cfd8fc --- /dev/null +++ b/src/authly/token_provider.cr @@ -0,0 +1,100 @@ +module Authly + module TokenProvider + abstract def encode(payload : Hash(String, String | Bool | Int64)) : String + + abstract def decode(token : String) : Hash(String, String | Bool | Int64) + + abstract def valid?(token : String) : Bool + + abstract def revoke(token : String) + + abstract def revoke?(token : String) : Bool + + abstract def introspect(token : String) : Hash(String, String | Bool | Int64) + end + + # JWT Token Provider + class JWTTokenProvider + include TokenProvider + + def encode(payload : Hash(String, String | Bool | Int64)) : String + JWT.encode(payload, Authly.config.security.secret_key, Authly.config.security.algorithm) + end + + def decode(token : String) : Hash(String, String | Bool | Int64) + decoded_token, _header = JWT.decode(token, Authly.config.security.public_key, Authly.config.security.algorithm) + decoded_token + end + + def valid?(token : String) : Bool + decoded_token, _header = JWT.decode(token, Authly.config.security.public_key, Authly.config.security.algorithm) + exp = decoded_token["exp"].to_s.to_i + Time.utc.to_unix < exp + rescue JWT::DecodeError + false + end + + def revoke(token : String) + Authly.config.providers.jti_provider.revoke(token) + end + + def revoke?(token : String) : Bool + Authly.config.providers.jti_provider.revoked?(token) + end + + def introspect(token : String) : Hash(String, String | Bool | Int64) + payload = decode(token) + active = valid?(token) + { + "active" => active, + "scope" => payload["scope"], + "cid" => payload["cid"], + "exp" => payload["exp"], + "sub" => payload["sub"] + } + rescue JWT::DecodeError + {"active" => false} + end + end + + # Opaque Token Provider + class OpaqueTokenProvider + include TokenProvider + + def encode(payload : Hash(String, String | Bool | Int64)) : String + token = Random::Secure.hex(32) + Authly.config.providers.jti_provider.store(token, payload) + token + end + + def decode(token : String) : Hash(String, String | Bool | Int64) + Authly.config.providers.jti_provider.fetch(token) || raise Error.invalid_token + end + + def valid?(token : String) : Bool + !Authly.config.providers.jti_provider.revoked?(token) + end + + def revoke(token : String) + Authly.config.providers.jti_provider.revoke(token) + end + + def revoke?(token : String) : Bool + Authly.config.providers.jti_provider.revoked?(token) + end + + def introspect(token : String) : Hash(String, String | Bool | Int64) + payload = decode(token) + active = valid?(token) + { + "active" => active, + "scope" => payload["scope"], + "cid" => payload["cid"], + "exp" => payload["exp"], + "sub" => payload["sub"] + } + rescue Error + {"active" => false} + end + end +end