Commit efdfe318 authored by theworldbright's avatar theworldbright
Browse files

Add ability to get user info from access tokens

parent a1f3d5f5
......@@ -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"
......
......@@ -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
class CreateTokens < ActiveRecord::Migration
def change
def self.up
create_table :tokens do |t|
t.belongs_to :o_auth_application
t.string :token
......
@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
# 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
......
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
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
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
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
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
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
require 'rspec'
describe TokenEndpoint do
it "shoud generate a token" do
end
end
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment