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
|
||||
|
||||
scope :with_redirect_uri, ->(given_uri) { where redirect_uri: given_uri }
|
||||
|
||||
def setup
|
||||
self.refresh_token = SecureRandom.hex(32)
|
||||
end
|
||||
|
|
@ -25,12 +27,18 @@ module Api
|
|||
end
|
||||
end
|
||||
|
||||
def create_code
|
||||
self.code = SecureRandom.hex(32)
|
||||
save
|
||||
self.code
|
||||
end
|
||||
|
||||
def create_access_token
|
||||
o_auth_access_tokens.create!.bearer_token
|
||||
# TODO: Add support for request object
|
||||
end
|
||||
|
||||
def create_id_token(nonce)
|
||||
def create_id_token(nonce=nil)
|
||||
id_tokens.create!(nonce: nonce)
|
||||
end
|
||||
|
||||
|
|
@ -44,6 +52,11 @@ module Api
|
|||
o_auth_applications: {client_id: client_id}, refresh_token: refresh_token)
|
||||
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
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ module Api
|
|||
|
||||
class << self
|
||||
def available_response_types
|
||||
["id_token", "id_token token"]
|
||||
["id_token", "id_token token", "code"]
|
||||
end
|
||||
|
||||
def register!(registrar)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ class CreateAuthorizations < ActiveRecord::Migration
|
|||
t.belongs_to :user, index: true
|
||||
t.belongs_to :o_auth_application, index: true
|
||||
t.string :refresh_token
|
||||
t.string :code
|
||||
t.string :redirect_uri
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ ActiveRecord::Schema.define(version: 20150724152052) do
|
|||
t.integer "user_id", limit: 4
|
||||
t.integer "o_auth_application_id", limit: 4
|
||||
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 "updated_at", null: false
|
||||
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
|
||||
|
||||
# TODO: Add support for request object and auth code
|
||||
# TODO: Add support for request object
|
||||
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
|
||||
handle_approved_response_type(auth, req, res)
|
||||
res.approve!
|
||||
|
|
@ -29,10 +30,16 @@ module Api
|
|||
|
||||
def handle_approved_response_type(auth, req, res)
|
||||
response_types = Array(req.response_type)
|
||||
handle_approved_auth_code(auth, res, response_types)
|
||||
handle_approved_access_token(auth, res, response_types)
|
||||
handle_approved_id_token(auth, req, res, response_types)
|
||||
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)
|
||||
return unless response_types.include?(:token)
|
||||
res.access_token = auth.create_access_token
|
||||
|
|
@ -41,8 +48,9 @@ module Api
|
|||
def handle_approved_id_token(auth, req, res, response_types)
|
||||
return unless response_types.include?(:id_token)
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@ module Api
|
|||
handle_password_flow(o_auth_app, req, res)
|
||||
when :refresh_token
|
||||
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
|
||||
req.unsupported_grant_type!
|
||||
end
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do
|
|||
|
||||
context "when redirect uri is missing" 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
|
||||
# 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.
|
||||
|
|
@ -94,7 +94,8 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do
|
|||
end
|
||||
end
|
||||
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
|
||||
before do
|
||||
|
|
@ -188,5 +189,40 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -3,14 +3,78 @@ require "spec_helper"
|
|||
describe Api::OpenidConnect::TokenEndpoint, type: :request do
|
||||
let!(:client) do
|
||||
Api::OpenidConnect::OAuthApplication.create!(
|
||||
redirect_uris: ["http://localhost"], client_name: "diaspora client")
|
||||
redirect_uris: ["http://localhost:3000/"], client_name: "diaspora client")
|
||||
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
|
||||
Api::OpenidConnect::Scope.find_or_create_by(name: "read")
|
||||
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
|
||||
context "when the username field is missing" do
|
||||
it "should return an invalid request error" do
|
||||
|
|
@ -92,7 +156,7 @@ describe Api::OpenidConnect::TokenEndpoint, type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe "the refresh token flow" do
|
||||
describe "the refresh token grant type" do
|
||||
context "when the refresh token is valid" do
|
||||
it "should return an access token" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "refresh_token",
|
||||
|
|
|
|||
Loading…
Reference in a new issue