Allow current user to be obtained from access token

This commit is contained in:
theworldbright 2015-07-06 20:31:52 +09:00 committed by theworldbright
parent 68d96a3189
commit beae77102d
18 changed files with 116 additions and 79 deletions

View file

@ -1,6 +1,5 @@
class Api::V2::BaseController < ApplicationController class Api::V2::BaseController < ApplicationController
include Openid::Authentication include OpenidConnect::Authentication
before_action :authenticate_user!
before_filter :require_access_token before_filter :require_access_token
end end

View file

@ -1,5 +1,11 @@
class Api::V2::UsersController < Api::V2::BaseController class Api::V2::UsersController < Api::V2::BaseController
def show def show
render json: current_user render json: user
end
private
def user
current_token.o_auth_application.user
end end
end end

View file

@ -1,4 +1,7 @@
class OAuthApplication < ActiveRecord::Base class OAuthApplication < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :client_id, presence: true, uniqueness: true validates :client_id, presence: true, uniqueness: true
validates :client_secret, presence: true validates :client_secret, presence: true

View file

@ -246,7 +246,7 @@ Diaspora::Application.routes.draw do
resources :authorizations, only: [:new, :create] resources :authorizations, only: [:new, :create]
match 'connect', to: 'connect#show', via: [:get, :post] match 'connect', to: 'connect#show', via: [:get, :post]
match '.well-known/:id', to: 'discovery#show' , via: [:get, :post] match '.well-known/:id', to: 'discovery#show' , via: [:get, :post]
post 'access_tokens', to: proc { |env| Openid::TokenEndpoint.new.call(env) } post 'access_tokens', to: proc { |env| OpenidConnect::TokenEndpoint.new.call(env) }
end end
api_version(:module => "Api::V2", path: {value: "api/v2"}, default: true) do api_version(:module => "Api::V2", path: {value: "api/v2"}, default: true) do

View file

@ -1,10 +1,15 @@
class CreateOAuthApplications < ActiveRecord::Migration class CreateOAuthApplications < ActiveRecord::Migration
def change def self.up
create_table :o_auth_applications do |t| create_table :o_auth_applications do |t|
t.belongs_to :user, index: true
t.string :client_id t.string :client_id
t.string :client_secret t.string :client_secret
t.timestamps null: false t.timestamps null: false
end end
end end
def self.down
drop_table :o_auth_applications
end
end end

View file

@ -1,7 +1,7 @@
class CreateTokens < ActiveRecord::Migration class CreateTokens < ActiveRecord::Migration
def self.up def self.up
create_table :tokens do |t| create_table :tokens do |t|
t.belongs_to :o_auth_application t.belongs_to :o_auth_application, index: true
t.string :token t.string :token
t.datetime :expires_at t.datetime :expires_at
t.timestamps null: false t.timestamps null: false

View file

@ -237,12 +237,15 @@ ActiveRecord::Schema.define(version: 20151003142048) do
add_index "notifications", ["target_type", "target_id"], name: "index_notifications_on_target_type_and_target_id", length: {"target_type"=>190, "target_id"=>nil}, using: :btree add_index "notifications", ["target_type", "target_id"], name: "index_notifications_on_target_type_and_target_id", length: {"target_type"=>190, "target_id"=>nil}, using: :btree
create_table "o_auth_applications", force: :cascade do |t| create_table "o_auth_applications", force: :cascade do |t|
t.integer "user_id", limit: 4
t.string "client_id", limit: 255 t.string "client_id", limit: 255
t.string "client_secret", limit: 255 t.string "client_secret", 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
add_index "o_auth_applications", ["user_id"], name: "index_o_auth_applications_on_user_id", using: :btree
create_table "o_embed_caches", force: :cascade do |t| create_table "o_embed_caches", force: :cascade do |t|
t.string "url", limit: 1024, null: false t.string "url", limit: 1024, null: false
t.text "data", limit: 65535, null: false t.text "data", limit: 65535, null: false
@ -543,6 +546,8 @@ ActiveRecord::Schema.define(version: 20151003142048) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "tokens", ["o_auth_application_id"], name: "index_tokens_on_o_auth_application_id", using: :btree
create_table "user_preferences", force: :cascade do |t| create_table "user_preferences", force: :cascade do |t|
t.string "email_type", limit: 255 t.string "email_type", limit: 255
t.integer "user_id", limit: 4 t.integer "user_id", limit: 4

View file

@ -0,0 +1,19 @@
@javascript
Feature: Access protected resources using password flow
# TODO: Add tests for expired access tokens
Background:
Given a user with username "kent"
Scenario: Valid bearer tokens sent via Authorization Request Header Field
Scenario: Valid bearer tokens sent via Form Encoded Parameter
Scenario: Valid bearer tokens sent via URI query parameter
When I send a post request to the token endpoint using "kent"'s credentials
And I use received valid bearer tokens to access user info via URI query parameter
Then I should receive "kent"'s id, username, and email
Scenario: Invalid bearer tokens sent via URI query parameter
When I send a post request to the token endpoint using "kent"'s credentials
And I use invalid bearer tokens to access user info via URI query parameter
Then I should receive an "invalid_token" error

View file

@ -1,31 +0,0 @@
@javascript
# TODO: Add tests for expired access tokens
# TODO: Add tests to check for WWW-Authenticate response header field as according to RFC 6750
Feature: Access protected resources using bearer access token
Background:
Given a user with username "bob"
And I log in manually as "bob" with password "password"
And I send a post request to the token endpoint using "bob"'s credentials
Scenario: Valid bearer tokens sent via Authorization Request Header Field
# TODO: Add tests
Scenario: Valid bearer tokens sent via Form Encoded Parameter
# TODO: Add tests
Scenario: Valid bearer tokens sent via URI query parameter
When I use received valid bearer tokens to access user info via URI query parameter
Then I should receive "bob"'s id, username, and email
# TODO: I want to confirm that the cache-control header in the response is private as according to RFC 6750
# Unfortunately, selenium doesn't allow access to response headers
Scenario: Invalid bearer tokens sent via URI query parameter
When I use invalid bearer tokens to access user info via URI query parameter
Then I should receive an "invalid_token" error
Scenario: Valid bearer tokens sent via URI query parameter but user is logged out
When I log out manually
And I use received valid bearer tokens to access user info via URI query parameter
Then I should see "Sign in" in the content
When I log in manually as "bob" with password "password"
Then I should receive "bob"'s id, username, and email

View file

@ -10,13 +10,13 @@ end
When /^I use received valid bearer tokens to access user info via URI query parameter$/ do When /^I use received valid bearer tokens to access user info via URI query parameter$/ do
accessTokenJson = JSON.parse(last_response.body) accessTokenJson = JSON.parse(last_response.body)
userInfoEndPointURL = "/openid/user_info/" userInfoEndPointURL = "/api/v2/user/"
userInfoEndPointURLQuery = "?access_token=" + accessTokenJson["access_token"] userInfoEndPointURLQuery = "?access_token=" + accessTokenJson["access_token"]
visit userInfoEndPointURL + userInfoEndPointURLQuery visit userInfoEndPointURL + userInfoEndPointURLQuery
end end
When /^I use invalid bearer tokens to access user info via URI query parameter$/ do When /^I use invalid bearer tokens to access user info via URI query parameter$/ do
userInfoEndPointURL = "/openid/user_info/" userInfoEndPointURL = "/api/v2/user/"
userInfoEndPointURLQuery = "?access_token=" + SecureRandom.hex(32) userInfoEndPointURLQuery = "?access_token=" + SecureRandom.hex(32)
visit userInfoEndPointURL + userInfoEndPointURLQuery visit userInfoEndPointURL + userInfoEndPointURLQuery
end end

View file

@ -1,35 +0,0 @@
module Openid
class TokenEndpoint
attr_accessor :app
delegate :call, to: :app
def initialize
@app = Rack::OAuth2::Server::Token.new do |req, res|
case req.grant_type
when :password
o_auth_app = retrieveOrCreateNewClientApplication(req)
user = User.find_for_database_authentication(username: req.username)
if o_auth_app && user && user.valid_password?(req.password)
res.access_token = o_auth_app.tokens.create!.bearer_token
else
req.invalid_grant!
end
else
res.unsupported_grant_type!
end
end
end
def retrieveOrCreateNewClientApplication(req)
retrieveClient(req) || createClient(req)
end
def retrieveClient(req)
OAuthApplication.find_by_client_id req.client_id
end
def createClient(req)
OAuthApplication.create!(client_id: req.client_id, client_secret: req.client_secret)
end
end
end

View file

@ -1,4 +1,4 @@
module Openid module OpenidConnect
module Authentication module Authentication
def self.included(klass) def self.included(klass)
@ -22,9 +22,9 @@ module Openid
end end
end end
# Scopes should be implemented here # TODO: Scopes should be implemented here
def required_scopes def required_scopes
nil # as default nil
end end
end end
end end

View file

@ -1,4 +1,4 @@
module Openid module OpenidConnect
class AuthorizationEndpoint class AuthorizationEndpoint
attr_accessor :app, :account, :client, :redirect_uri, :response_type, :scopes, :_request_, :request_uri, :request_object attr_accessor :app, :account, :client, :redirect_uri, :response_type, :scopes, :_request_, :request_uri, :request_object
delegate :call, to: :app delegate :call, to: :app

View file

@ -0,0 +1,39 @@
module OpenidConnect
class TokenEndpoint
attr_accessor :app
delegate :call, to: :app
def initialize
@app = Rack::OAuth2::Server::Token.new do |req, res|
case req.grant_type
when :password
user = User.find_for_database_authentication(username: req.username)
if user
o_auth_app = retrieveOrCreateNewClientApplication(req, user)
if o_auth_app && user.valid_password?(req.password)
res.access_token = o_auth_app.tokens.create!.bearer_token
else
req.invalid_grant!
end
else
req.invalid_grant! # TODO: Change to user login
end
else
res.unsupported_grant_type!
end
end
end
def retrieveOrCreateNewClientApplication(req, user)
retrieveClient(req, user) || createClient(req, user)
end
def retrieveClient(req, user)
user.o_auth_applications.find_by_client_id req.client_id
end
def createClient(req, user)
user.o_auth_applications.create!(client_id: req.client_id, client_secret: req.client_secret)
end
end
end

View file

@ -0,0 +1,19 @@
require 'spec_helper'
# TODO: Confirm that the cache-control header in the response is private as according to RFC 6750
# TODO: Check for WWW-Authenticate response header field as according to RFC 6750
describe Api::V2::UsersController, type: :request do
describe "#show" do
let!(:application) { bob.o_auth_applications.create!(client_id: 1, client_secret: "secret") }
let!(:token) { application.tokens.create!.bearer_token.to_s }
context "when valid" do
it "shows the user's info" do
get "/api/v2/user/?access_token=" + token
jsonBody = JSON.parse(response.body)
expect(jsonBody["username"]).to eq(bob.username)
expect(jsonBody["email"]).to eq(bob.email)
end
end
end
end

View file

@ -1,6 +1,6 @@
require 'spec_helper' require 'spec_helper'
describe "Token Endpoint", type: :request do describe OpenidConnect::TokenEndpoint, type: :request do
describe "password grant type" do describe "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

View file

@ -59,6 +59,14 @@ def photo_fixture_name
@photo_fixture_name = File.join(File.dirname(__FILE__), "fixtures", "button.png") @photo_fixture_name = File.join(File.dirname(__FILE__), "fixtures", "button.png")
end end
def retrieveAccessToken(user)
o_auth_app = OAuthApplication.create!(client_id: 4, client_secret: "azerty")
user = User.find_for_database_authentication(username: user.username)
if o_auth_app && user && user.valid_password?("bluepin7") # Hard coded password for bob
o_auth_app.tokens.create!.bearer_token.to_s
end
end
# Force fixture rebuild # Force fixture rebuild
FileUtils.rm_f(Rails.root.join("tmp", "fixture_builder.yml")) FileUtils.rm_f(Rails.root.join("tmp", "fixture_builder.yml"))