Add ability to get user info from access tokens
This commit is contained in:
parent
a1f3d5f5f9
commit
efdfe318fd
18 changed files with 295 additions and 46 deletions
3
Gemfile
3
Gemfile
|
|
@ -279,6 +279,9 @@ group :test do
|
|||
gem "database_cleaner" , "1.5.1"
|
||||
gem "selenium-webdriver", "2.47.1"
|
||||
|
||||
gem "cucumber-api-steps", "0.13", require: false
|
||||
gem "json_spec", "1.1.4"
|
||||
|
||||
# General helpers
|
||||
|
||||
gem "factory_girl_rails", "4.5.0"
|
||||
|
|
|
|||
56
Gemfile.lock
56
Gemfile.lock
|
|
@ -57,6 +57,7 @@ GEM
|
|||
ast (2.2.0)
|
||||
astrolabe (1.3.1)
|
||||
parser (~> 2.2)
|
||||
attr_required (1.0.0)
|
||||
autoprefixer-rails (6.2.2)
|
||||
execjs
|
||||
json
|
||||
|
|
@ -66,6 +67,7 @@ GEM
|
|||
jquery-rails
|
||||
railties
|
||||
bcrypt (3.1.10)
|
||||
bindata (2.1.0)
|
||||
bootstrap-sass (3.3.6)
|
||||
autoprefixer-rails (>= 5.2.1)
|
||||
sass (>= 3.3.4)
|
||||
|
|
@ -126,6 +128,10 @@ GEM
|
|||
gherkin (~> 2.12)
|
||||
multi_json (>= 1.7.5, < 2.0)
|
||||
multi_test (>= 0.1.2)
|
||||
cucumber-api-steps (0.13)
|
||||
cucumber (>= 1.2.1)
|
||||
jsonpath (>= 0.1.2)
|
||||
rspec (>= 2.12.0)
|
||||
cucumber-rails (1.4.2)
|
||||
capybara (>= 1.1.2, < 3)
|
||||
cucumber (>= 1.3.8, < 2)
|
||||
|
|
@ -390,6 +396,7 @@ GEM
|
|||
httparty (0.13.7)
|
||||
json (~> 1.8)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.7.1)
|
||||
i18n (0.7.0)
|
||||
i18n-inflector (2.6.7)
|
||||
i18n (>= 0.4.1)
|
||||
|
|
@ -423,8 +430,19 @@ GEM
|
|||
multi_json (>= 1.3)
|
||||
rake
|
||||
json (1.8.3)
|
||||
json-jwt (1.5.1)
|
||||
activesupport
|
||||
bindata
|
||||
multi_json (>= 1.3)
|
||||
securecompare
|
||||
url_safe_base64
|
||||
json-schema (2.5.2)
|
||||
addressable (~> 2.3.8)
|
||||
json_spec (1.1.4)
|
||||
multi_json (~> 1.0)
|
||||
rspec (>= 2.0, < 4.0)
|
||||
jsonpath (0.5.7)
|
||||
multi_json
|
||||
jwt (1.5.2)
|
||||
kaminari (0.16.3)
|
||||
actionpack (>= 3.0.0)
|
||||
|
|
@ -504,6 +522,17 @@ GEM
|
|||
open_graph_reader (0.6.1)
|
||||
faraday (~> 0.9.0)
|
||||
nokogiri (~> 1.6)
|
||||
openid_connect (0.9.2)
|
||||
activemodel
|
||||
attr_required (>= 1.0.0)
|
||||
json (>= 1.4.3)
|
||||
json-jwt (>= 1.5.0)
|
||||
rack-oauth2 (>= 1.2.1)
|
||||
swd (>= 1.0.0)
|
||||
tzinfo
|
||||
validate_email
|
||||
validate_url
|
||||
webfinger (>= 1.0.1)
|
||||
orm_adapter (0.5.0)
|
||||
parser (2.2.3.0)
|
||||
ast (>= 1.1, < 3.0)
|
||||
|
|
@ -545,6 +574,12 @@ GEM
|
|||
activesupport
|
||||
rack-mobile-detect (0.4.0)
|
||||
rack
|
||||
rack-oauth2 (1.2.1)
|
||||
activesupport (>= 2.3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.4)
|
||||
multi_json (>= 1.3.6)
|
||||
rack (>= 1.1)
|
||||
rack-piwik (0.3.0)
|
||||
rack-pjax (0.8.0)
|
||||
nokogiri (~> 1.5)
|
||||
|
|
@ -708,6 +743,7 @@ GEM
|
|||
scss_lint (0.42.2)
|
||||
rainbow (~> 2.0)
|
||||
sass (~> 3.4.15)
|
||||
securecompare (1.0.0)
|
||||
selenium-webdriver (2.47.1)
|
||||
childprocess (~> 0.5)
|
||||
multi_json (~> 1.0)
|
||||
|
|
@ -757,6 +793,12 @@ GEM
|
|||
activesupport (>= 3.0)
|
||||
sprockets (>= 2.8, < 4.0)
|
||||
state_machine (1.2.0)
|
||||
swd (1.0.0)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.4)
|
||||
i18n
|
||||
json (>= 1.4.3)
|
||||
sysexits (1.2.0)
|
||||
systemu (2.6.5)
|
||||
terminal-table (1.5.2)
|
||||
|
|
@ -797,11 +839,22 @@ GEM
|
|||
kgio (~> 2.6)
|
||||
rack
|
||||
raindrops (~> 0.7)
|
||||
url_safe_base64 (0.2.2)
|
||||
uuid (2.3.8)
|
||||
macaddr (~> 1.0)
|
||||
valid (1.1.0)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
validate_url (1.0.2)
|
||||
activemodel (>= 3.0.0)
|
||||
addressable
|
||||
warden (1.2.4)
|
||||
rack (>= 1.0)
|
||||
webfinger (1.0.1)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
multi_json
|
||||
webmock (1.22.3)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
|
|
@ -830,6 +883,7 @@ DEPENDENCIES
|
|||
carrierwave (= 0.10.0)
|
||||
compass-rails (= 2.0.5)
|
||||
configurate (= 0.3.1)
|
||||
cucumber-api-steps (= 0.13)
|
||||
cucumber-rails (= 1.4.2)
|
||||
database_cleaner (= 1.5.1)
|
||||
devise (= 3.5.3)
|
||||
|
|
@ -867,6 +921,7 @@ DEPENDENCIES
|
|||
jshintrb (= 0.3.0)
|
||||
json (= 1.8.3)
|
||||
json-schema (= 2.5.2)
|
||||
json_spec (= 1.1.4)
|
||||
leaflet-rails (= 0.7.4)
|
||||
logging-rails (= 0.5.0)
|
||||
markerb (= 1.1.0)
|
||||
|
|
@ -882,6 +937,7 @@ DEPENDENCIES
|
|||
omniauth-twitter (= 1.2.1)
|
||||
omniauth-wordpress (= 0.2.2)
|
||||
open_graph_reader (= 0.6.1)
|
||||
openid_connect
|
||||
pg (= 0.18.4)
|
||||
pronto (= 0.5.3)
|
||||
pronto-haml (= 0.5.0)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,17 @@
|
|||
# the COPYRIGHT file.
|
||||
|
||||
class UsersController < ApplicationController
|
||||
before_action :authenticate_user!, :except => [:new, :create, :public, :user_photo]
|
||||
include Openid::Authentication
|
||||
|
||||
before_action :authenticate_user!, except: [:new, :create, :public, :user_photo]
|
||||
before_filter :require_access_token, only: [:show]
|
||||
respond_to :html
|
||||
|
||||
# TODO: Adjust so that it sends back only required elements, e.g, should not send hashed password (serialized_private_key) back
|
||||
def show
|
||||
render json: current_user
|
||||
end
|
||||
|
||||
def edit
|
||||
@aspect = :user_edit
|
||||
@user = current_user
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ class Token < ActiveRecord::Base
|
|||
|
||||
validates :token, presence: true, uniqueness: true
|
||||
|
||||
scope :valid, ->(time) { where("expires_at >= ?", time) }
|
||||
|
||||
def setup
|
||||
self.token = SecureRandom.hex(32)
|
||||
self.token = SecureRandom.hex(32)
|
||||
self.expires_at = 24.hours.from_now
|
||||
end
|
||||
|
||||
|
|
@ -16,4 +18,8 @@ class Token < ActiveRecord::Base
|
|||
expires_in: (expires_at - Time.now.utc).to_i
|
||||
)
|
||||
end
|
||||
|
||||
def accessible?(_scopes_or_claims_ = nil)
|
||||
true # TODO: For now don't support scopes
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ module Diaspora
|
|||
# Custom directories with classes and modules you want to be autoloadable.
|
||||
config.autoload_paths += %W{#{config.root}/app}
|
||||
config.autoload_once_paths += %W{#{config.root}/lib}
|
||||
config.autoload_paths += %W{#{config.root}/lib/openid_connect}
|
||||
config.autoload_paths += %W{#{config.root}/lib/openid}
|
||||
|
||||
# Only load the plugins named here, in the order given (default is alphabetical).
|
||||
# :all can be used as a placeholder for all plugins not explicitly named.
|
||||
|
|
@ -108,5 +108,9 @@ module Diaspora
|
|||
host: AppConfig.pod_uri.authority
|
||||
}
|
||||
config.action_mailer.asset_host = AppConfig.pod_uri.to_s
|
||||
|
||||
config.middleware.use Rack::OAuth2::Server::Resource::Bearer, 'OpenID Connect' do |req|
|
||||
Token.valid(Time.now.utc).find_by(token: req.access_token) || req.invalid_token!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ Diaspora::Application.routes.draw do
|
|||
resources :authorizations, only: [:new, :create]
|
||||
match 'connect', to: 'connect#show', via: [:get, :post]
|
||||
match '.well-known/:id', to: 'discovery#show' , :via => [:get, :post]
|
||||
match 'user_info', to: 'user#show', :via => [:get, :post]
|
||||
post 'access_tokens', to: proc { |env| TokenEndpoint.new.call(env) }
|
||||
post 'access_tokens', to: proc { |env| Openid::TokenEndpoint.new.call(env) }
|
||||
match 'user_info', to: 'users#show', :via => [:get, :post]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
class CreateTokens < ActiveRecord::Migration
|
||||
def change
|
||||
def self.up
|
||||
create_table :tokens do |t|
|
||||
t.belongs_to :o_auth_application
|
||||
t.string :token
|
||||
|
|
|
|||
31
features/desktop/protected_resource.feature
Normal file
31
features/desktop/protected_resource.feature
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@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
|
||||
37
features/step_definitions/openid_steps.rb
Normal file
37
features/step_definitions/openid_steps.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Password has been hard coded as all test accounts seem to have a password of "password"
|
||||
Given /^I send a post request to the token endpoint using "([^\"]*)"'s credentials$/ do |username|
|
||||
user = User.find_by(username: username)
|
||||
tokenEndpointURL = "/openid/access_tokens"
|
||||
tokenEndpointURLQuery = "?grant_type=password&username=" +
|
||||
user.username +
|
||||
"&password=password&client_id=4&client_secret=azerty"
|
||||
post tokenEndpointURL + tokenEndpointURLQuery
|
||||
end
|
||||
|
||||
When /^I use received valid bearer tokens to access user info via URI query parameter$/ do
|
||||
accessTokenJson = JSON.parse(last_response.body)
|
||||
userInfoEndPointURL = "/openid/user_info/"
|
||||
userInfoEndPointURLQuery = "?access_token=" + accessTokenJson["access_token"]
|
||||
visit userInfoEndPointURL + userInfoEndPointURLQuery
|
||||
end
|
||||
|
||||
When /^I use invalid bearer tokens to access user info via URI query parameter$/ do
|
||||
userInfoEndPointURL = "/openid/user_info/"
|
||||
userInfoEndPointURLQuery = "?access_token=" + SecureRandom.hex(32)
|
||||
visit userInfoEndPointURL + userInfoEndPointURLQuery
|
||||
end
|
||||
|
||||
Then /^I should receive "([^\"]*)"'s id, username, and email$/ do |username|
|
||||
user = User.find_by_username(username)
|
||||
expect(page).to have_content(user.username)
|
||||
expect(page).to have_content(user.language)
|
||||
expect(page).to have_content(user.email)
|
||||
end
|
||||
|
||||
Then /^I should receive an "([^\"]*)" error$/ do |error_message|
|
||||
expect(page).to have_content(error_message)
|
||||
end
|
||||
|
||||
Then /^I should see "([^\"]*)" in the content$/ do |content|
|
||||
expect(page).to have_content(content)
|
||||
end
|
||||
|
|
@ -12,6 +12,9 @@ require "capybara/cucumber"
|
|||
require "capybara/session"
|
||||
require "selenium/webdriver"
|
||||
|
||||
require "cucumber/api_steps"
|
||||
require "json_spec/cucumber"
|
||||
|
||||
# Ensure we know the appservers port
|
||||
Capybara.server_port = AppConfig.pod_uri.port
|
||||
Rails.application.routes.default_url_options[:host] = AppConfig.pod_uri.host
|
||||
|
|
|
|||
30
lib/openid/authentication.rb
Normal file
30
lib/openid/authentication.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
module Openid
|
||||
module Authentication
|
||||
|
||||
def self.included(klass)
|
||||
klass.send :include, Authentication::Helper
|
||||
end
|
||||
|
||||
module Helper
|
||||
def current_token
|
||||
@current_token
|
||||
end
|
||||
end
|
||||
|
||||
def require_access_token
|
||||
@current_token = request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
|
||||
unless @current_token
|
||||
raise Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new("Unauthorized user")
|
||||
end
|
||||
# TODO: This block is useless until we actually start checking for scopes
|
||||
unless @current_token.try(:accessible?, required_scopes)
|
||||
raise Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(:insufficient_scope)
|
||||
end
|
||||
end
|
||||
|
||||
# Scopes should be implemented here
|
||||
def required_scopes
|
||||
nil # as default
|
||||
end
|
||||
end
|
||||
end
|
||||
12
lib/openid/authorization_endpoint.rb
Normal file
12
lib/openid/authorization_endpoint.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
module Openid
|
||||
class AuthorizationEndpoint
|
||||
attr_accessor :app, :account, :client, :redirect_uri, :response_type, :scopes, :_request_, :request_uri, :request_object
|
||||
delegate :call, to: :app
|
||||
|
||||
def initialize(allow_approval = false, approved = false)
|
||||
@app = Rack::OAuth2::Server::Authorize.new do |req, res|
|
||||
req.unsupported_response_type! # TODO: not supported yet
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
35
lib/openid/token_endpoint.rb
Normal file
35
lib/openid/token_endpoint.rb
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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,10 +0,0 @@
|
|||
class AuthorizationEndpoint
|
||||
attr_accessor :app, :account, :client, :redirect_uri, :response_type, :scopes, :_request_, :request_uri, :request_object
|
||||
delegate :call, to: :app
|
||||
|
||||
def initialize(allow_approval = false, approved = false)
|
||||
@app = Rack::OAuth2::Server::Authorize.new do |req, res|
|
||||
req.unsupported_response_type! # Not supported yet
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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
|
||||
# If the grant type is password, the application does not have to be known
|
||||
# If it does not exist, insert into DB
|
||||
user = User.find_for_database_authentication(username: req.username)
|
||||
o_auth_app = OAuthApplication.find_by_client_id req.client_id
|
||||
o_auth_app ||= OAuthApplication.create!(client_id: req.client_id, client_secret: req.client_secret)
|
||||
if user.valid_password? req.password
|
||||
res.access_token = o_auth_app.tokens.create!.bearer_token
|
||||
end
|
||||
else
|
||||
req.unsupported_grant_type!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
64
spec/integration/openid/token_endpoint_spec.rb
Normal file
64
spec/integration/openid/token_endpoint_spec.rb
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe "Token Endpoint", type: :request do
|
||||
describe "password grant type" do
|
||||
context "when the username field is missing" do
|
||||
it "should return an invalid request error" do
|
||||
post "/openid/access_tokens?grant_type=password\&password=bluepin7\&client_id=4\&client_secret=azerty"
|
||||
expect(response.body).to include("'username' required")
|
||||
end
|
||||
end
|
||||
context "when the password field is missing" do
|
||||
it "should return an invalid request error" do
|
||||
post "/openid/access_tokens?grant_type=password\&username=bob\&client_id=4\&client_secret=azerty"
|
||||
expect(response.body).to include("'password' required")
|
||||
end
|
||||
end
|
||||
context "when the username does not match an existing user" do
|
||||
it "should return an invalid request error" do
|
||||
post "/openid/access_tokens?grant_type=password\&username=mewasdfrandom\&password=bluepin7\&client_id=4\&client_secret=azerty"
|
||||
expect(response.body).to include("invalid_grant")
|
||||
end
|
||||
end
|
||||
context "when the password is invalid" do
|
||||
it "should return an invalid request error" do
|
||||
post "/openid/access_tokens?grant_type=password\&username=mewasdfrandom\&password=bluepin7\&client_id=4\&client_secret=azerty"
|
||||
expect(response.body).to include("invalid_grant")
|
||||
end
|
||||
end
|
||||
context "when there are duplicate fields" do
|
||||
it "should return an invalid request error" do
|
||||
post "/openid/access_tokens?grant_type=password\&username=bob\&password=bluepin6\&username=bob\&password=bluepin7\&client_id=4\&client_secret=azerty"
|
||||
expect(response.body).to include("invalid_grant")
|
||||
# TODO: Apparently Nov's implementation lets this one pass; however, according to the OIDC spec, we are supposed to reject duplicate fields. Is this a security issue?
|
||||
end
|
||||
end
|
||||
context "when the client is unauthorized" do
|
||||
# TODO: If we support password grant, we should prevent access from unauthorized client applications
|
||||
it "should return an error" do
|
||||
fail
|
||||
end
|
||||
end
|
||||
context "when many unauthorized requests are made" do
|
||||
# TODO: If we support password grant, we should support a way to prevent brute force attacks (using rate-limitation or generating alerts) as specified by RFC 6749 4.3.2 Access Token Request
|
||||
it "should generate an alert" do
|
||||
fail
|
||||
end
|
||||
end
|
||||
context "when the request is valid" do
|
||||
it "should return an access token" do
|
||||
post "/openid/access_tokens?grant_type=password\&username=bob\&password=bluepin7\&client_id=4\&client_secret=azerty"
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["access_token"].length).to eq(64)
|
||||
expect(json["token_type"]).to eq("bearer")
|
||||
expect(json.keys).to include("expires_in")
|
||||
end
|
||||
end
|
||||
end
|
||||
describe "unsupported grant type" do
|
||||
it "should return an unsupported grant type error" do
|
||||
post "/openid/access_tokens?grant_type=me\&username=bob\&password=bluepin7\&client_id=4\&client_secret=azerty"
|
||||
expect(response.body).to include "unsupported_grant_type"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
require 'rspec'
|
||||
|
||||
describe TokenEndpoint do
|
||||
|
||||
it "shoud generate a token" do
|
||||
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue