Add support for authorization code flow

This commit is contained in:
theworldbright 2015-07-31 01:35:50 +09:00
parent bc5e5c7420
commit e5932968fd
10 changed files with 202 additions and 10 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",