diff --git a/app/controllers/api/v2/base_controller.rb b/app/controllers/api/v2/base_controller.rb index 33bfb9d71..a9d85bcc4 100644 --- a/app/controllers/api/v2/base_controller.rb +++ b/app/controllers/api/v2/base_controller.rb @@ -1,6 +1,5 @@ class Api::V2::BaseController < ApplicationController - include Openid::Authentication + include OpenidConnect::Authentication - before_action :authenticate_user! before_filter :require_access_token end diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb index 0a6d34114..7f8ea55b7 100644 --- a/app/controllers/api/v2/users_controller.rb +++ b/app/controllers/api/v2/users_controller.rb @@ -1,5 +1,11 @@ class Api::V2::UsersController < Api::V2::BaseController + def show - render json: current_user + render json: user + end + +private + def user + current_token.o_auth_application.user end end diff --git a/app/models/o_auth_application.rb b/app/models/o_auth_application.rb index 29d3cd87e..11ed470c1 100644 --- a/app/models/o_auth_application.rb +++ b/app/models/o_auth_application.rb @@ -1,4 +1,7 @@ class OAuthApplication < ActiveRecord::Base + belongs_to :user + + validates :user_id, presence: true validates :client_id, presence: true, uniqueness: true validates :client_secret, presence: true diff --git a/config/routes.rb b/config/routes.rb index 3cb9501cb..02d62211f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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] - post 'access_tokens', to: proc { |env| Openid::TokenEndpoint.new.call(env) } + post 'access_tokens', to: proc { |env| OpenidConnect::TokenEndpoint.new.call(env) } end api_version(:module => "Api::V2", path: {value: "api/v2"}, default: true) do diff --git a/db/migrate/20150613202109_create_o_auth_applications.rb b/db/migrate/20150613202109_create_o_auth_applications.rb index 1ad4638d2..b9309064d 100644 --- a/db/migrate/20150613202109_create_o_auth_applications.rb +++ b/db/migrate/20150613202109_create_o_auth_applications.rb @@ -1,10 +1,15 @@ class CreateOAuthApplications < ActiveRecord::Migration - def change + def self.up create_table :o_auth_applications do |t| + t.belongs_to :user, index: true t.string :client_id t.string :client_secret t.timestamps null: false end end + + def self.down + drop_table :o_auth_applications + end end diff --git a/db/migrate/20150614134031_create_tokens.rb b/db/migrate/20150614134031_create_tokens.rb index 613a7f53d..9db140791 100644 --- a/db/migrate/20150614134031_create_tokens.rb +++ b/db/migrate/20150614134031_create_tokens.rb @@ -1,7 +1,7 @@ class CreateTokens < ActiveRecord::Migration def self.up create_table :tokens do |t| - t.belongs_to :o_auth_application + t.belongs_to :o_auth_application, index: true t.string :token t.datetime :expires_at t.timestamps null: false diff --git a/db/schema.rb b/db/schema.rb index 77fd2e4c4..7b36511f8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 create_table "o_auth_applications", force: :cascade do |t| + t.integer "user_id", limit: 4 t.string "client_id", limit: 255 t.string "client_secret", limit: 255 t.datetime "created_at", null: false t.datetime "updated_at", null: false 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| t.string "url", limit: 1024, 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 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| t.string "email_type", limit: 255 t.integer "user_id", limit: 4 diff --git a/features/desktop/oauth_password_flow.feature b/features/desktop/oauth_password_flow.feature new file mode 100644 index 000000000..4e28d825f --- /dev/null +++ b/features/desktop/oauth_password_flow.feature @@ -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 diff --git a/features/desktop/protected_resource.feature b/features/desktop/protected_resource.feature deleted file mode 100644 index 4d0cee1ec..000000000 --- a/features/desktop/protected_resource.feature +++ /dev/null @@ -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 diff --git a/features/step_definitions/openid_steps.rb b/features/step_definitions/openid_steps.rb index 180711c35..ef1974da8 100644 --- a/features/step_definitions/openid_steps.rb +++ b/features/step_definitions/openid_steps.rb @@ -10,13 +10,13 @@ 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/" + userInfoEndPointURL = "/api/v2/user/" 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/" + userInfoEndPointURL = "/api/v2/user/" userInfoEndPointURLQuery = "?access_token=" + SecureRandom.hex(32) visit userInfoEndPointURL + userInfoEndPointURLQuery end diff --git a/lib/openid/token_endpoint.rb b/lib/openid/token_endpoint.rb deleted file mode 100644 index ae52a0305..000000000 --- a/lib/openid/token_endpoint.rb +++ /dev/null @@ -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 diff --git a/lib/openid/LICENSE b/lib/openid_connect/LICENSE similarity index 100% rename from lib/openid/LICENSE rename to lib/openid_connect/LICENSE diff --git a/lib/openid/authentication.rb b/lib/openid_connect/authentication.rb similarity index 90% rename from lib/openid/authentication.rb rename to lib/openid_connect/authentication.rb index 7423ba21e..eca5573a0 100644 --- a/lib/openid/authentication.rb +++ b/lib/openid_connect/authentication.rb @@ -1,4 +1,4 @@ -module Openid +module OpenidConnect module Authentication def self.included(klass) @@ -22,9 +22,9 @@ module Openid end end - # Scopes should be implemented here + # TODO: Scopes should be implemented here def required_scopes - nil # as default + nil end end end diff --git a/lib/openid/authorization_endpoint.rb b/lib/openid_connect/authorization_endpoint.rb similarity index 95% rename from lib/openid/authorization_endpoint.rb rename to lib/openid_connect/authorization_endpoint.rb index 3b8ea9a55..61fbe11ce 100644 --- a/lib/openid/authorization_endpoint.rb +++ b/lib/openid_connect/authorization_endpoint.rb @@ -1,4 +1,4 @@ -module Openid +module OpenidConnect class AuthorizationEndpoint attr_accessor :app, :account, :client, :redirect_uri, :response_type, :scopes, :_request_, :request_uri, :request_object delegate :call, to: :app diff --git a/lib/openid_connect/token_endpoint.rb b/lib/openid_connect/token_endpoint.rb new file mode 100644 index 000000000..070ae4549 --- /dev/null +++ b/lib/openid_connect/token_endpoint.rb @@ -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 diff --git a/spec/controllers/api/v2/users_controller_spec.rb b/spec/controllers/api/v2/users_controller_spec.rb new file mode 100644 index 000000000..fd2230962 --- /dev/null +++ b/spec/controllers/api/v2/users_controller_spec.rb @@ -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 diff --git a/spec/integration/openid/token_endpoint_spec.rb b/spec/lib/openid_connect/token_endpoint_spec.rb similarity index 98% rename from spec/integration/openid/token_endpoint_spec.rb rename to spec/lib/openid_connect/token_endpoint_spec.rb index 2d8598177..4325a05e0 100644 --- a/spec/integration/openid/token_endpoint_spec.rb +++ b/spec/lib/openid_connect/token_endpoint_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Token Endpoint", type: :request do +describe OpenidConnect::TokenEndpoint, type: :request do describe "password grant type" do context "when the username field is missing" do it "should return an invalid request error" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ca444256a..dcdb16d05 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -59,6 +59,14 @@ def photo_fixture_name @photo_fixture_name = File.join(File.dirname(__FILE__), "fixtures", "button.png") 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 FileUtils.rm_f(Rails.root.join("tmp", "fixture_builder.yml"))