Add support for authorization code flow
This commit is contained in:
parent
bc5e5c7420
commit
e5932968fd
10 changed files with 202 additions and 10 deletions
|
|
@ -15,6 +15,8 @@ module Api
|
||||||
|
|
||||||
before_validation :setup, on: :create
|
before_validation :setup, on: :create
|
||||||
|
|
||||||
|
scope :with_redirect_uri, ->(given_uri) { where redirect_uri: given_uri }
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
self.refresh_token = SecureRandom.hex(32)
|
self.refresh_token = SecureRandom.hex(32)
|
||||||
end
|
end
|
||||||
|
|
@ -25,12 +27,18 @@ module Api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_code
|
||||||
|
self.code = SecureRandom.hex(32)
|
||||||
|
save
|
||||||
|
self.code
|
||||||
|
end
|
||||||
|
|
||||||
def create_access_token
|
def create_access_token
|
||||||
o_auth_access_tokens.create!.bearer_token
|
o_auth_access_tokens.create!.bearer_token
|
||||||
# TODO: Add support for request object
|
# TODO: Add support for request object
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_id_token(nonce)
|
def create_id_token(nonce=nil)
|
||||||
id_tokens.create!(nonce: nonce)
|
id_tokens.create!(nonce: nonce)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -44,6 +52,11 @@ module Api
|
||||||
o_auth_applications: {client_id: client_id}, refresh_token: refresh_token)
|
o_auth_applications: {client_id: client_id}, refresh_token: refresh_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.use_code(code)
|
||||||
|
auth = find_by(code: code)
|
||||||
|
auth.code = nil if auth # Remove auth code if found so it can't be reused
|
||||||
|
auth
|
||||||
|
end
|
||||||
# TODO: Consider splitting into subclasses by flow type
|
# TODO: Consider splitting into subclasses by flow type
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ module Api
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def available_response_types
|
def available_response_types
|
||||||
["id_token", "id_token token"]
|
["id_token", "id_token token", "code"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def register!(registrar)
|
def register!(registrar)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ class CreateAuthorizations < ActiveRecord::Migration
|
||||||
t.belongs_to :user, index: true
|
t.belongs_to :user, index: true
|
||||||
t.belongs_to :o_auth_application, index: true
|
t.belongs_to :o_auth_application, index: true
|
||||||
t.string :refresh_token
|
t.string :refresh_token
|
||||||
|
t.string :code
|
||||||
|
t.string :redirect_uri
|
||||||
|
|
||||||
t.timestamps null: false
|
t.timestamps null: false
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ ActiveRecord::Schema.define(version: 20150724152052) do
|
||||||
t.integer "user_id", limit: 4
|
t.integer "user_id", limit: 4
|
||||||
t.integer "o_auth_application_id", limit: 4
|
t.integer "o_auth_application_id", limit: 4
|
||||||
t.string "refresh_token", limit: 255
|
t.string "refresh_token", limit: 255
|
||||||
|
t.string "code", limit: 255
|
||||||
|
t.string "redirect_uri", limit: 255
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
end
|
||||||
|
|
|
||||||
27
features/desktop/oidc_auth_code_flow.feature
Normal file
27
features/desktop/oidc_auth_code_flow.feature
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
@javascript
|
||||||
|
Feature: Access protected resources using auth code flow
|
||||||
|
Background:
|
||||||
|
Given a user with username "kent"
|
||||||
|
And all scopes exist
|
||||||
|
|
||||||
|
Scenario: Invalid client id to auth endpoint
|
||||||
|
When I register a new client
|
||||||
|
And I send a post request from that client to the code flow authorization endpoint using a invalid client id
|
||||||
|
And I sign in as "kent@kent.kent"
|
||||||
|
Then I should see an "bad_request" error
|
||||||
|
|
||||||
|
Scenario: Application is denied authorization
|
||||||
|
When I register a new client
|
||||||
|
And I send a post request from that client to the code flow authorization endpoint
|
||||||
|
And I sign in as "kent@kent.kent"
|
||||||
|
And I deny authorization to the client
|
||||||
|
Then I should not see any tokens in the redirect url
|
||||||
|
|
||||||
|
Scenario: Application is authorized
|
||||||
|
When I register a new client
|
||||||
|
And I send a post request from that client to the code flow authorization endpoint
|
||||||
|
And I sign in as "kent@kent.kent"
|
||||||
|
And I give my consent and authorize the client
|
||||||
|
And I parse the auth code and create a request to the token endpoint
|
||||||
|
And I parse the tokens and use it obtain user info
|
||||||
|
Then I should receive "kent"'s id, username, and email
|
||||||
32
features/step_definitions/auth_code_steps.rb
Normal file
32
features/step_definitions/auth_code_steps.rb
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
o_auth_query_params = %i(
|
||||||
|
redirect_uri=http://localhost:3000
|
||||||
|
response_type=code
|
||||||
|
scope=openid%20read
|
||||||
|
nonce=hello
|
||||||
|
state=hi
|
||||||
|
).join("&")
|
||||||
|
|
||||||
|
Given /^I send a post request from that client to the code flow authorization endpoint$/ do
|
||||||
|
client_json = JSON.parse(last_response.body)
|
||||||
|
@client_id = client_json['o_auth_application']['client_id']
|
||||||
|
@client_secret = client_json['o_auth_application']['client_secret']
|
||||||
|
visit new_api_openid_connect_authorization_path +
|
||||||
|
"?client_id=#{@client_id}&#{o_auth_query_params}"
|
||||||
|
end
|
||||||
|
|
||||||
|
Given /^I send a post request from that client to the code flow authorization endpoint using a invalid client id/ do
|
||||||
|
visit new_api_openid_connect_authorization_path + "?client_id=randomid&#{o_auth_query_params}"
|
||||||
|
end
|
||||||
|
|
||||||
|
When /^I parse the auth code and create a request to the token endpoint$/ do
|
||||||
|
code = current_url[/(?<=code=)[^&]+/]
|
||||||
|
post api_openid_connect_access_tokens_path, code: code,
|
||||||
|
redirect_uri: "http://localhost:3000", grant_type: "authorization_code",
|
||||||
|
client_id: @client_id, client_secret: @client_secret
|
||||||
|
end
|
||||||
|
|
||||||
|
When /^I parse the tokens and use it obtain user info$/ do
|
||||||
|
client_json = JSON.parse(last_response.body)
|
||||||
|
access_token = client_json["access_token"]
|
||||||
|
get api_v0_user_path, access_token: access_token
|
||||||
|
end
|
||||||
|
|
@ -19,9 +19,10 @@ module Api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Add support for request object and auth code
|
# TODO: Add support for request object
|
||||||
def approved!(req, res)
|
def approved!(req, res)
|
||||||
auth = OpenidConnect::Authorization.find_or_create_by(o_auth_application: @o_auth_application, user: @user)
|
auth = OpenidConnect::Authorization.find_or_create_by(
|
||||||
|
o_auth_application: @o_auth_application, user: @user, redirect_uri: @redirect_uri)
|
||||||
auth.scopes << @scopes
|
auth.scopes << @scopes
|
||||||
handle_approved_response_type(auth, req, res)
|
handle_approved_response_type(auth, req, res)
|
||||||
res.approve!
|
res.approve!
|
||||||
|
|
@ -29,10 +30,16 @@ module Api
|
||||||
|
|
||||||
def handle_approved_response_type(auth, req, res)
|
def handle_approved_response_type(auth, req, res)
|
||||||
response_types = Array(req.response_type)
|
response_types = Array(req.response_type)
|
||||||
|
handle_approved_auth_code(auth, res, response_types)
|
||||||
handle_approved_access_token(auth, res, response_types)
|
handle_approved_access_token(auth, res, response_types)
|
||||||
handle_approved_id_token(auth, req, res, response_types)
|
handle_approved_id_token(auth, req, res, response_types)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_approved_auth_code(auth, res, response_types)
|
||||||
|
return unless response_types.include?(:code)
|
||||||
|
res.code = auth.create_code
|
||||||
|
end
|
||||||
|
|
||||||
def handle_approved_access_token(auth, res, response_types)
|
def handle_approved_access_token(auth, res, response_types)
|
||||||
return unless response_types.include?(:token)
|
return unless response_types.include?(:token)
|
||||||
res.access_token = auth.create_access_token
|
res.access_token = auth.create_access_token
|
||||||
|
|
@ -41,8 +48,9 @@ module Api
|
||||||
def handle_approved_id_token(auth, req, res, response_types)
|
def handle_approved_id_token(auth, req, res, response_types)
|
||||||
return unless response_types.include?(:id_token)
|
return unless response_types.include?(:id_token)
|
||||||
id_token = auth.create_id_token(req.nonce)
|
id_token = auth.create_id_token(req.nonce)
|
||||||
|
auth_code_value = res.respond_to?(:code) ? res.code : nil
|
||||||
access_token_value = res.respond_to?(:access_token) ? res.access_token : nil
|
access_token_value = res.respond_to?(:access_token) ? res.access_token : nil
|
||||||
res.id_token = id_token.to_jwt(code: nil, access_token: access_token_value)
|
res.id_token = id_token.to_jwt(code: auth_code_value, access_token: access_token_value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,14 @@ module Api
|
||||||
handle_password_flow(o_auth_app, req, res)
|
handle_password_flow(o_auth_app, req, res)
|
||||||
when :refresh_token
|
when :refresh_token
|
||||||
handle_refresh_flow(req, res)
|
handle_refresh_flow(req, res)
|
||||||
|
when :authorization_code
|
||||||
|
auth = Api::OpenidConnect::Authorization.with_redirect_uri(req.redirect_uri).use_code(req.code)
|
||||||
|
req.invalid_grant! if auth.blank?
|
||||||
|
res.access_token = auth.create_access_token
|
||||||
|
if auth.accessible?(Api::OpenidConnect::Scope.find_by(name: "openid"))
|
||||||
|
id_token = auth.create_id_token
|
||||||
|
res.id_token = id_token.to_jwt(access_token: res.access_token)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
req.unsupported_grant_type!
|
req.unsupported_grant_type!
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do
|
||||||
|
|
||||||
context "when redirect uri is missing" do
|
context "when redirect uri is missing" do
|
||||||
context "when only one redirect URL is pre-registered" do
|
context "when only one redirect URL is pre-registered" do
|
||||||
it "should return a form pager" do
|
it "should return a form page" do
|
||||||
# Note this intentionally behavior diverts from OIDC spec http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
# Note this intentionally behavior diverts from OIDC spec http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
# When client has only one redirect uri registered, only that redirect uri can be used. Hence,
|
# When client has only one redirect uri registered, only that redirect uri can be used. Hence,
|
||||||
# we should implicitly assume the client wants to use that registered URI.
|
# we should implicitly assume the client wants to use that registered URI.
|
||||||
|
|
@ -94,7 +94,8 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
context "when already authorized" do
|
context "when already authorized" do
|
||||||
let!(:auth) { Api::OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: alice) }
|
let!(:auth) { Api::OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: alice,
|
||||||
|
redirect_uri: "http://localhost:3000/") }
|
||||||
|
|
||||||
context "when valid parameters are passed" do
|
context "when valid parameters are passed" do
|
||||||
before do
|
before do
|
||||||
|
|
@ -188,5 +189,40 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when code" do
|
||||||
|
before do
|
||||||
|
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "code",
|
||||||
|
scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when authorization is approved" do
|
||||||
|
before do
|
||||||
|
post :create, approve: "true"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return the code" do
|
||||||
|
expect(response.location).to have_content("code")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return the passed in state" do
|
||||||
|
expect(response.location).to have_content("state=4180930983")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when authorization is denied" do
|
||||||
|
before do
|
||||||
|
post :create, approve: "false"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return an error" do
|
||||||
|
expect(response.location).to have_content("error")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should NOT contain code" do
|
||||||
|
expect(response.location).to_not have_content("code")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,78 @@ require "spec_helper"
|
||||||
describe Api::OpenidConnect::TokenEndpoint, type: :request do
|
describe Api::OpenidConnect::TokenEndpoint, type: :request do
|
||||||
let!(:client) do
|
let!(:client) do
|
||||||
Api::OpenidConnect::OAuthApplication.create!(
|
Api::OpenidConnect::OAuthApplication.create!(
|
||||||
redirect_uris: ["http://localhost"], client_name: "diaspora client")
|
redirect_uris: ["http://localhost:3000/"], client_name: "diaspora client")
|
||||||
end
|
end
|
||||||
let(:auth) { Api::OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: bob) }
|
let!(:auth) {
|
||||||
|
Api::OpenidConnect::Authorization.find_or_create_by(
|
||||||
|
o_auth_application: client, user: bob, redirect_uri: "http://localhost:3000/")
|
||||||
|
}
|
||||||
|
let!(:code) { auth.create_code }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Api::OpenidConnect::Scope.find_or_create_by(name: "read")
|
Api::OpenidConnect::Scope.find_or_create_by(name: "read")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "the authorization code grant type" do
|
||||||
|
context "when the authorization code is valid" do
|
||||||
|
before do
|
||||||
|
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||||
|
client_id: client.client_id, client_secret: client.client_secret,
|
||||||
|
redirect_uri: "http://localhost:3000/", code: code
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return a valid id token" do
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
encoded_id_token = json["id_token"]
|
||||||
|
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||||
|
Api::OpenidConnect::IdTokenConfig.public_key
|
||||||
|
expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return a valid access token" do
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
encoded_id_token = json["id_token"]
|
||||||
|
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||||
|
Api::OpenidConnect::IdTokenConfig.public_key
|
||||||
|
access_token = json["access_token"]
|
||||||
|
access_token_check_num = UrlSafeBase64.encode64(OpenSSL::Digest::SHA256.digest(access_token)[0, 128 / 8])
|
||||||
|
expect(decoded_token.at_hash).to eq(access_token_check_num)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the authorization code is not valid" do
|
||||||
|
it "should return an invalid grant error" do
|
||||||
|
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||||
|
client_id: client.client_id, client_secret: client.client_secret, code: "123456"
|
||||||
|
expect(response.body).to include "invalid_grant"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the client is unregistered" do
|
||||||
|
it "should return an error" do
|
||||||
|
post api_openid_connect_access_tokens_path, grant_type: "authorization_code", code: auth.refresh_token,
|
||||||
|
client_id: SecureRandom.hex(16).to_s, client_secret: client.client_secret
|
||||||
|
expect(response.body).to include "invalid_client"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the code field is missing" do
|
||||||
|
it "should return an invalid request error" do
|
||||||
|
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||||
|
client_id: client.client_id, client_secret: client.client_secret
|
||||||
|
expect(response.body).to include "invalid_request"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the client_secret doesn't match" do
|
||||||
|
it "should return an invalid client error" do
|
||||||
|
post api_openid_connect_access_tokens_path, grant_type: "authorization_code", code: auth.refresh_token,
|
||||||
|
client_id: client.client_id, client_secret: "client.client_secret"
|
||||||
|
expect(response.body).to include "invalid_client"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "the password grant type" do
|
describe "the password grant type" do
|
||||||
context "when the username field is missing" do
|
context "when the username field is missing" do
|
||||||
it "should return an invalid request error" do
|
it "should return an invalid request error" do
|
||||||
|
|
@ -92,7 +156,7 @@ describe Api::OpenidConnect::TokenEndpoint, type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "the refresh token flow" do
|
describe "the refresh token grant type" do
|
||||||
context "when the refresh token is valid" do
|
context "when the refresh token is valid" do
|
||||||
it "should return an access token" do
|
it "should return an access token" do
|
||||||
post api_openid_connect_access_tokens_path, grant_type: "refresh_token",
|
post api_openid_connect_access_tokens_path, grant_type: "refresh_token",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue