Skip to content

Commit

Permalink
Refactor token handling with strategy pattern
Browse files Browse the repository at this point in the history
Replaced direct JWT encoding/decoding calls with a strategy pattern using a `TokenManager` class and `TokenProvider` interface. This introduces support for multiple token types (JWT and opaque) and centralizes token management. Enhanced configuration to handle different token types and improved error handling with detailed messages for unsupported operations. This change aims to increase code flexibility and maintainability, allowing easy addition of new token strategies in the future.
  • Loading branch information
eliasjpr committed Oct 8, 2024
1 parent e25e89e commit 5073197
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 43 deletions.
4 changes: 2 additions & 2 deletions spec/authly_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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({
Expand Down
51 changes: 16 additions & 35 deletions src/authly.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions src/authly/access_token.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/authly/code.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module Authly
end

def jwt
Authly.jwt_encode({
Authly.encode_token({
"code" => code,
"challenge" => challenge,
"method" => method,
Expand Down
1 change: 1 addition & 0 deletions src/authly/configuration.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
5 changes: 5 additions & 0 deletions src/authly/error.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/authly/grant.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/authly/grants/refresh_token.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions src/authly/token_manager.cr
Original file line number Diff line number Diff line change
@@ -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
100 changes: 100 additions & 0 deletions src/authly/token_provider.cr
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 5073197

Please sign in to comment.