Skip to content

Commit

Permalink
Set shard version 1.2.6
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] authored and eliasjpr committed Oct 11, 2024
1 parent b13fc02 commit f75129a
Show file tree
Hide file tree
Showing 24 changed files with 445 additions and 142 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Authly provides HTTP handlers to set up OAuth2 endpoints. The available endpoint

```crystal
server = HTTP::Server.new([
Authly::OAuthHandler.new,
Authly::Handler.new,
])
server.bind_tcp("127.0.0.1", 8080)
server.listen
Expand All @@ -211,7 +211,7 @@ To integrate Authly into your existing application, create an instance of the se
require "authly"
server = HTTP::Server.new([
Authly::OAuthHandler.new,
Authly::Handler.new,
])
server.bind_tcp("0.0.0.0", 8080)
puts "Listening on http://0.0.0.0:8080"
Expand Down
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: authly
version: 1.2.5
version: 1.2.6
authors:
- Elias Perez <elias.perez@nytimes.com>
crystal: '>=0.31.1'
Expand Down
95 changes: 95 additions & 0 deletions spec/grant_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Spec file for testing the Grant class and its compliance with OAuth2 specifications
require "./spec_helper"

module Authly
describe Grant do
client_id = "1"
client_secret = "secret"
username = "username"
password = "password"
redirect_uri = "hhttps://www.example.com/callback"
refresh_token = "test_refresh_token"
authorization_code = Authly::Code.new(client_id, "read", redirect_uri, "", "", username).to_s

it "creates an access token with valid client credentials grant" do
grant = Grant.new("client_credentials", client_id: client_id, client_secret: client_secret)
token = grant.token

token.client_id.should eq client_id
token.scope.should eq ""
end

it "creates an access token with valid password grant" do
grant = Grant.new("password",
client_id: client_id,
client_secret: client_secret,
username: username,
password: password)

token = grant.token

token.client_id.should eq client_id
token.scope.should eq ""
end

it "creates an access token with valid authorization code grant" do
grant = Grant.new("authorization_code",
client_id: client_id,
client_secret: client_secret,
redirect_uri: redirect_uri,
code: authorization_code)

token = grant.token

token.client_id.should eq client_id
token.scope.should eq "read"
end

it "creates an access token with valid refresh token grant" do
authorization_code = Authly::Code.new(client_id, "read", redirect_uri, "", "", username).to_s

grant = Grant.new("refresh_token",
client_id: client_id,
client_secret: client_secret,
refresh_token: refresh_token,
code: authorization_code)

token = grant.token

token.client_id.should eq client_id
token.scope.should eq ""
end

it "raises error for unsupported grant type" do
expect_raises(Error) { Grant.new("unsupported_grant_type", client_id: client_id, client_secret: client_secret) }
end

it "validates scope for access token" do
grant = Grant.new("authorization_code",
client_id: client_id,
client_secret: client_secret,
redirect_uri: redirect_uri,
code: authorization_code)

grant.code = Authly.jwt_encode({"scope" => "read"})
token = grant.token

token.scope.should eq "read"
end

it "raises error for invalid scope" do
scope = "pluto"
invalid_scope = "mars"
Authly.clients << Authly::Client.new("new_client", "secret", "https://www.example.com/callback", "2", scope)
authorization_code = Authly::Code.new("2", invalid_scope, redirect_uri, "", "", username).to_s

grant = Grant.new("authorization_code",
client_id: "2",
client_secret: client_secret,
redirect_uri: redirect_uri,
code: authorization_code)

expect_raises(Error) { grant.token }
end
end
end
2 changes: 0 additions & 2 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@ require "base64"
require "faker"
require "../src/authly"
require "./support/settings"

BASE_URI = "http://0.0.0.0:4000"
38 changes: 32 additions & 6 deletions spec/support/handlers_spec.cr
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
# Spec tests for Authly's OAuth Handlers
require "./spec_helper"
require "../spec_helper"
require "http/server"

module Authly
describe "AuthorizationHandler" do
xit "returns authorization code with valid client_id and redirect_uri" do
response = HTTP::Client.get("#{BASE_URI}/oauth/authorize?client_id=1&redirect_uri=https://www.example.com/callback&response_type=code")
it "returns authorization code with valid client_id and redirect_uri after user consent and includes state" do
state = "test_state"
# Initial Authorize Request
initia_response = HTTP::Client.get("#{BASE_URI}/oauth/authorize?client_id=1&redirect_uri=https://www.example.com/callback&response_type=code&state=#{state}")

# Consent Request
response = HTTP::Client.get("#{BASE_URI}/oauth/authorize?client_id=1&redirect_uri=https://www.example.com/callback&response_type=code&state=#{state}&consent=approved")

response.status_code.should eq 302
response.headers["Location"].should_not be_nil
response.headers["Location"].should contain(URI.encode_path("code="))
response.headers["Location"].should contain(URI.encode_path("state=#{state}"))
end

it "renders consent page if user consent is not provided" do
state = "test_state"
response = HTTP::Client.get("#{BASE_URI}/oauth/authorize?client_id=1&redirect_uri=https://www.example.com/callback&response_type=code&state=#{state}")

response.status_code.should eq 200
response.body.should contain("Authorization Request")
response.body.should contain("Approve")
response.body.should contain("Deny")
response.body.should contain(state)
end

xit "returns 401 for invalid client_id or redirect_uri" do
response = HTTP::Client.get("#{BASE_URI}/oauth/authorize?client_id=invalid&redirect_uri=invalid")
it "returns 401 for invalid client_id or redirect_uri" do
state = "test_state"
response = HTTP::Client.get("#{BASE_URI}/oauth/authorize?client_id=invalid&redirect_uri=invalid&state=#{state}&consent=approved")
response.status_code.should eq 401
response.body.should eq "This client is not authorized to use the requested grant type"
end

it "returns 400 for invalid state parameter" do
state = "invalid_state"
response = HTTP::Client.get("#{BASE_URI}/oauth/authorize?client_id=1&redirect_uri=https://www.example.com/callback&response_type=code&state=#{state}&consent=approved")

response.status_code.should eq 400
response.body.should eq "Invalid state parameter"
end
end

describe "TokenHandler" do
Expand All @@ -30,7 +57,6 @@ module Authly
})
response.status_code.should eq 200
body = JSON.parse(response.body)
body["access_token"]
body["access_token"].should_not be_nil
end

Expand Down
6 changes: 6 additions & 0 deletions spec/support/settings.cr
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
secret_key = "4bce37fbb1542a68dddba2da22635beca9d814cb3424c461fcc8876904ad39c1"
BASE_URI = "http://0.0.0.0:4000"
STATE_STORE = Authly::InMemoryStateStore.new

Authly.configure do |config|
config.secret_key = secret_key
config.public_key = secret_key
config.state_store = STATE_STORE
end

Authly.clients << Authly::Client.new("example", "secret", "https://www.example.com/callback", "1")
Authly.owners << Authly::Owner.new("username", "password")

OAUTH_HANDLER = Authly::Handler.new
6 changes: 4 additions & 2 deletions spec/support/test_server.cr
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
require "http/server"
require "../src/authly"
require "../../src/authly"
require "./settings"

server = HTTP::Server.new([
Authly::OAuthHandler.new,
Authly::Handler.new,
])
server.bind_tcp "0.0.0.0", 4000
puts "Listening on http://0.0.0.0:4000"

server.listen
2 changes: 1 addition & 1 deletion src/authly.cr
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module Authly
end

def self.jwt_decode(token, secret_key = config.public_key)
JWT.decode token, secret_key, config.algorithm
JWT.decode(token, secret_key, config.algorithm, iss: config.issuer)
end

def self.revoke(token)
Expand Down
1 change: 1 addition & 0 deletions src/authly/access_token.cr
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module Authly
"sub" => @client_id,
"name" => "refresh token",
"iat" => Time.utc.to_unix,
"iss" => Authly.config.issuer,
"exp" => REFRESH_TTL.from_now.to_unix,
})
end
Expand Down
1 change: 1 addition & 0 deletions src/authly/authorizable_client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ module Authly
module AuthorizableClient
abstract def valid_redirect?(client_id : String, redirect_uri : String) : Bool
abstract def authorized?(client_id : String, client_secret : String)
abstract def allowed_scopes?(client_id : String, scopes : String) : Bool
end
end
13 changes: 13 additions & 0 deletions src/authly/client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,18 @@ module Authly
def each(& : Client -> _)
@clients.each { |client| yield client }
end

def allowed_scopes?(client_id, scopes) : Bool
client = self.find! do |client|
client.id == client_id
end
return false unless client

client.scopes.split(" ").all? do |scope|
scopes.split(" ").includes?(scope)
end
rescue
false
end
end
end
2 changes: 2 additions & 0 deletions src/authly/code.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Authly
struct Code
include JSON::Serializable
CODE_TTL = Authly.config.code_ttl
ISSUER = Authly.config.issuer

getter code : String = Random::Secure.hex(16),
client_id : String,
Expand Down Expand Up @@ -30,6 +31,7 @@ module Authly
"user_id" => user_id,
"redirect_uri" => redirect_uri,
"iat" => Time.utc.to_unix,
"iss" => ISSUER,
"exp" => CODE_TTL.from_now.to_unix,
})
end
Expand Down
1 change: 1 addition & 0 deletions src/authly/configuration.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ module Authly
property token_store : TokenStore = InMemoryStore.new
property algorithm : JWT::Algorithm = JWT::Algorithm::HS256
property token_strategy : Symbol = :jwt
property state_store : StateStore = InMemoryStateStore.new
end
end
55 changes: 38 additions & 17 deletions src/authly/grant.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ module Authly
case grant_type
when "authorization_code"
AuthorizationCode.new(
args["client_id"],
args["client_secret"],
args.fetch("redirect_uri", ""),
args.fetch("challenge", ""),
args.fetch("method", ""),
args.fetch("verifier", "")
client_id: args[:client_id],
client_secret: args[:client_secret],
redirect_uri: args.fetch(:redirect_uri, ""),
challenge: args.fetch(:challenge, ""),
method: args.fetch(:method, ""),
verifier: args.fetch(:verifier, "")
)
when "client_credentials"
ClientCredentials.new(args["client_id"], args["client_secret"])
ClientCredentials.new(client_id: args[:client_id], client_secret: args[:client_secret])
when "password"
Password.new(
args["client_id"],
args["client_secret"],
args.fetch("username", ""),
args.fetch("password", "")
client_id: args[:client_id],
client_secret: args[:client_secret],
username: args.fetch(:username, ""),
password: args.fetch(:password, "")
)
when "refresh_token"
RefreshToken.new(
args["client_id"],
args["client_secret"],
args.fetch("refresh_token", "")
client_id: args["client_id"],
client_secret: args["client_secret"],
refresh_token: args.fetch("refresh_token", "")
)
else
raise Error.unsupported_grant_type
Expand All @@ -38,16 +38,25 @@ module Authly

class Grant
@grant_strategy : GrantStrategy
@code : String
property code : String
@client_id : String
@token_manager : TokenManager = TokenManager.instance
@refresh_token : String

def initialize(grant_type : String, **args)
@grant_strategy = GrantFactory.create(grant_type, **args)
@code = args.fetch("code", "")
@refresh_token = args.fetch(:refresh_token, "")
@client_id = args.fetch(:client_id, "")
@code = args.fetch(:code, "")
end

def token : AccessToken
validate_scope!
authorized?
access_token

token = access_token
revoke_old_refresh_token(token.access_token)
token
end

def authorized?
Expand Down Expand Up @@ -76,5 +85,17 @@ module Authly
return "" if @code.empty?
auth_code["scope"].as_s
end

private def validate_scope!
unless Authly.clients.allowed_scopes?(@client_id, scope)
raise Error.invalid_scope
end
end

private def revoke_old_refresh_token(token : String)
if @grant_strategy.is_a?(RefreshToken)
@token_manager.revoke(@refresh_token)
end
end
end
end
6 changes: 2 additions & 4 deletions src/authly/grants/refresh_token.cr
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
module Authly
class RefreshToken
include GrantStrategy
getter client_id : String,
client_secret : String,
refresh_token : String
getter client_id : String, client_secret : String, refresh_token : String

def initialize(@client_id, @client_secret, @refresh_token)
end
Expand All @@ -15,7 +13,7 @@ module Authly
end

private def validate_code!
Authly.jwt_decode refresh_token
Authly.jwt_decode(refresh_token)
rescue e
raise Error.invalid_grant
end
Expand Down
Loading

0 comments on commit f75129a

Please sign in to comment.