Allow current user to be obtained from access token
This commit is contained in:
parent
68d96a3189
commit
beae77102d
18 changed files with 116 additions and 79 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
19
features/desktop/oauth_password_flow.feature
Normal file
19
features/desktop/oauth_password_flow.feature
Normal 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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
39
lib/openid_connect/token_endpoint.rb
Normal file
39
lib/openid_connect/token_endpoint.rb
Normal 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
|
||||||
19
spec/controllers/api/v2/users_controller_spec.rb
Normal file
19
spec/controllers/api/v2/users_controller_spec.rb
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue