Commit 0e5426a
Changed files (3)
bin/idp
@@ -378,6 +378,50 @@ module Authz
end
end
+ class AuthorizationGrant
+ class << self
+ def all
+ @all ||= []
+ end
+
+ # TODO:: Look up saml_assertion
+ def find_by(params)
+ case params[:grant_type]
+ when "authorization_code"
+ # TODO:: implement `code_verifier` param
+ all.find do |grant|
+ grant.code == params[:code]
+ end
+ end
+ end
+
+ def create!(user)
+ new(user).tap do |grant|
+ all << grant
+ end
+ end
+ end
+
+ attr_reader :code, :user
+
+ def initialize(user)
+ @user = user
+ @code = SecureRandom.uuid
+ @exchanged_at = nil
+ end
+
+ def used?
+ @exchanged_at
+ end
+
+ def create_access_token
+ raise "Invalid code" if used?
+
+ @exchanged_at = Time.now
+ user.create_access_token
+ end
+ end
+
class OAuthController
include ::HTTPHelpers
@@ -407,10 +451,13 @@ module Authz
when "/oauth/authorize" # RFC-6749
return post_authorize(request)
when "/oauth/token" # RFC-6749
- # TODO:: Look up authorization grant by (code, saml_assertion)
- user = Authn::User.new(id: SecureRandom.uuid)
- return [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({
- access_token: user.create_access_token.to_jwt,
+ 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)
+ puts params.inspect
+ grant = AuthorizationGrant.find_by(params)
+
+ return [404, { "Content-Type" => "application/json" }, [JSON.pretty_generate({ error: 404, message: "Not Found" })]] if grant.nil? || grant.used?
+ return [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate({
+ access_token: grant.create_access_token.to_jwt,
token_type: "Bearer",
issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
expires_in: 3600,
@@ -453,13 +500,15 @@ module Authz
def post_authorize(request)
params = request.params.slice('client_id', 'redirect_uri', 'response_type', 'response_mode', 'state', 'code_challenge_method', 'code_challenge', 'scope')
+
+ grant = AuthorizationGrant.create!(current_user(request))
case params['response_type']
when 'code'
case params['response_mode']
when 'fragment'
- return [302, { 'Location' => "#{params['redirect_uri']}#code=#{SecureRandom.uuid}&state=#{params['state']}" }, []]
+ return [302, { 'Location' => "#{params['redirect_uri']}#code=#{grant.code}&state=#{params['state']}" }, []]
when 'query'
- return [302, { 'Location' => "#{params['redirect_uri']}?code=#{SecureRandom.uuid}&state=#{params['state']}" }, []]
+ return [302, { 'Location' => "#{params['redirect_uri']}?code=#{grant.code}&state=#{params['state']}" }, []]
else
# TODO:: form post
end
test/e2e_test.go
@@ -95,6 +95,20 @@ func TestAuthx(t *testing.T) {
assert.NotNil(t, organizations)
})
+ t.Run("GET http://api.example.com:8080/groups.json", func(t *testing.T) {
+ response := x.Must(http.Get("http://api.example.com:8080/groups.json"))
+ assert.Equal(t, http.StatusForbidden, response.StatusCode)
+ })
+
+ t.Run("GET http://api.example.com:8080/groups.json with Authorization", func(t *testing.T) {
+ request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/groups.json", nil))
+ request.Header.Add("Authorization", "Bearer "+item.AccessToken)
+ response := x.Must(client.Do(request))
+ require.Equal(t, http.StatusOK, response.StatusCode)
+ groups := x.Must(serde.FromJSON[[]map[string]string](response.Body))
+ assert.NotNil(t, groups)
+ })
+
t.Run("GET http://api.example.com:8080/projects.json", func(t *testing.T) {
response := x.Must(http.Get("http://api.example.com:8080/projects.json"))
assert.Equal(t, http.StatusForbidden, response.StatusCode)
@@ -242,6 +256,13 @@ func TestAuthx(t *testing.T) {
assert.Equal(t, "Bearer", credentials.TokenType)
assert.NotEmpty(t, credentials.RefreshToken)
+ t.Run("cannot re-use the same authorization grant", func(t *testing.T) {
+ newCredentials, err := conf.Exchange(ctx, code)
+
+ assert.Error(t, err)
+ assert.Empty(t, newCredentials)
+ })
+
t.Run("token is usable against REST API", func(t *testing.T) {
client := conf.Client(ctx, credentials)
response := x.Must(client.Get("http://api.example.com:8080/projects.json"))
policy.csv
@@ -1,8 +1,8 @@
-p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", api.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /*.json
+p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/\d+\z", api.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /*.json
p, *, *, (GET)|(HEAD), /health
p, *, *, GET, /.well-known/*
p, *, *, GET, /favicon.ico
-p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", idp.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /twirp/authx.rpc.*
+p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/\d+\z", idp.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /twirp/authx.rpc.*
p, *, idp.example.com, (GET)|(POST), /oauth*
p, *, idp.example.com, (GET)|(POST), /saml*
p, *, idp.example.com, (GET)|(POST), /sessions*