Skip to content

Commit

Permalink
Implement token revocation and validation
Browse files Browse the repository at this point in the history
Introduced the capability to revoke tokens and ensure their validity by adding a JTI provider and corresponding methods. The JTI (JWT ID) is used to uniquely identify each token, allowing for efficient tracking and revocation. The configuration now includes an issuer for token issuance, enhancing security and allowing for issuer-specific checks. Unit tests have been added to ensure proper functionality of the revoke and valid methods. This improves the system's security by preventing the use of revoked tokens.
  • Loading branch information
eliasjpr committed Oct 8, 2024
1 parent 44115c8 commit a785d5c
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 12 deletions.
42 changes: 42 additions & 0 deletions spec/authly_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe Authly do
client_secret: client_secret,
redirect_uri: redirect_uri,
code: code)

if id_token = token.id_token
id_token_decoded = Authly.jwt_decode(id_token).first

Expand Down Expand Up @@ -77,6 +78,47 @@ describe Authly do
end
end

describe ".revoke" do
it "revokes token" do
a_token = Authly::AccessToken.new(client_id, scope)
Authly.revoke(a_token.jti)
Authly.revoked?(a_token.jti).should eq true
end

it "does not revoke token" do
a_token = Authly::AccessToken.new(client_id, scope)
Authly.revoked?(a_token.jti).should eq false
end

it "does not revoke token" do
Authly.revoked?("invalid_jti").should eq false
end
end

describe ".valid?" do
it "returns true" do
a_token = Authly::AccessToken.new(client_id, scope)
token = a_token.access_token
Authly.valid?(token).should eq true
end

it "returns false" do
a_token = Authly::AccessToken.new(client_id, scope)
token = a_token.access_token

Authly.revoke(a_token.jti)

Authly.valid?(token).should eq false
end

it "returns false" do
a_token = Authly::AccessToken.new(client_id, scope)
token = a_token.access_token
Authly.revoke(a_token.jti)
Authly.valid?(token + "invalid").should eq false
end
end

describe ".introspect" do
it "returns active token" do
a_token = Authly::AccessToken.new(client_id, scope)
Expand Down
23 changes: 22 additions & 1 deletion src/authly.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ require "jwt"
require "json"
require "./authly/authorizable_owner"
require "./authly/authorizable_client"
require "./authly/jti_provider"
require "./authly/**"

require "log"
module Authly
CONFIG = Configuration.new

Expand Down Expand Up @@ -39,6 +40,26 @@ module Authly
JWT.decode token, secret_key, config.algorithm
end

def self.revoke(jti)
Authly.config.jti_provider.revoke(jti)
end

def self.revoked?(jti)
Authly.config.jti_provider.revoked?(jti)
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
end

def self.introspect(token : String)
# Decode the JWT, verify the signature and expiration
payload, _header = jwt_decode(token)
Expand Down
24 changes: 13 additions & 11 deletions src/authly/access_token.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,38 @@ module Authly
ACCESS_TTL = Authly.config.access_ttl
REFRESH_TTL = Authly.config.refresh_ttl

def self.jwt(client_id, scope, id_token)
new(client_id, scope, id_token).to_json
end

# The JWT ID (jti), used to track and revoke individual tokens
getter jti : String
getter access_token : String
getter token_type : String = "Bearer"
getter expires_in : Int64 = ACCESS_TTL.from_now.to_unix

getter revoked : Bool = false
@[JSON::Field(emit_null: false)]
getter id_token : String? = nil

getter refresh_token : String
@[JSON::Field(emit_null: false)]
getter refresh_token : String?

getter id_token : String? = nil
@[JSON::Field(ignore: true)]
getter client_id : String

def initialize(@client_id : String, @scope : String, @id_token = nil)
def self.jwt(client_id, scope, id_token)
new(client_id, scope, id_token).to_json
end

def initialize(@client_id : String, @scope : String, @id_token : String? = nil)
@jti = Random::Secure.hex(32) # Generate a unique jti for each token
@access_token = generate_token
@refresh_token = refresh_token
end

private def generate_token
Authly.jwt_encode({
"sub" => Random::Secure.hex(32),
"iss" => "The Oauth2 Server Provider",
"iss" => Authly.config.issuer,
"cid" => @client_id,
"iat" => Time.utc.to_unix,
"exp" => ACCESS_TTL.from_now.to_unix,
"scope" => @scope,
"jti" => @jti, # Include the jti in the token claims
})
end

Expand Down
2 changes: 2 additions & 0 deletions src/authly/configuration.cr
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
module Authly
class Configuration
property issuer : String = "The Authority Server Provider"
property secret_key : String = Random::Secure.hex(16)
property public_key : String = Random::Secure.hex(16)
property refresh_ttl : Time::Span = 1.day
property code_ttl : Time::Span = 5.minutes
property access_ttl : Time::Span = 1.hour
property owners : AuthorizableOwner = Owners.new
property clients : AuthorizableClient = Clients.new
property jti_provider : JTIProvider = InMemoryJTIProvider.new
property algorithm : JWT::Algorithm = JWT::Algorithm::HS256
end
end
26 changes: 26 additions & 0 deletions src/authly/in_memory_jti_provider.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Authly
class InMemoryJTIProvider
include Authly::JTIProvider
include Enumerable(String)

# Use a set to track revoked tokens by their jti
def initialize
@revoked_tokens = Set(String).new
end

# Implement the each method to make the class enumerable
def each
@revoked_tokens.each { |jti| yield jti }
end

# Method to revoke a token by its jti
def revoke(jti : String)
@revoked_tokens.add(jti)
end

# Method to check if a token's jti has been revoked
def revoked?(jti : String) : Bool
any? { |revoked_jti| revoked_jti == jti }
end
end
end
6 changes: 6 additions & 0 deletions src/authly/jti_provider.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Authly
module JTIProvider
abstract def revoke(jti : String)
abstract def revoked?(jti : String) : Bool
end
end

0 comments on commit a785d5c

Please sign in to comment.