commit
baeff22451
86 changed files with 3129 additions and 37 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -20,6 +20,7 @@ vendor/cache/
|
|||
config/database.yml
|
||||
.rvmrc_custom
|
||||
.rvmrc.local
|
||||
config/oidc_key.pem
|
||||
|
||||
# Mailing list stuff
|
||||
config/email_offset
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ bind to an UNIX socket at `unix:tmp/diaspora.sock`. Please change your local
|
|||
|
||||
With the port to Bootstrap 3, app/views/terms/default.haml has a new structure. If you have created a customised app/views/terms/terms.haml or app/views/terms/terms.erb file, you will need to edit those files to base your customisations on the new default.haml file.
|
||||
|
||||
## API authentication
|
||||
|
||||
This release makes diaspora\* a OpenID Connect provider. This means you can authenticate to third parties with your diaspora\* account and let
|
||||
them act as your diaspora* account on your behalf. This feature is still considered in early development, we still expect edge cases and advanced
|
||||
features of the specificiation to not be handled correctly or be missing. But we expect a basic OpenID Connect compliant client to work. Please submit issues!
|
||||
We will also most likely still change the authorization scopes we offer and started with a very minimal set.
|
||||
Most work still required is on documentation as well as designing and implementing the data API for all of Diaspora's functionality.
|
||||
Contributions are very welcome, the hard work is done!
|
||||
|
||||
## Refactor
|
||||
* Improve bookmarklet [#5904](https://github.com/diaspora/diaspora/pull/5904)
|
||||
* Update listen configuration to listen on unix sockets by default [#5974](https://github.com/diaspora/diaspora/pull/5974)
|
||||
|
|
|
|||
8
Gemfile
8
Gemfile
|
|
@ -149,6 +149,9 @@ gem "omniauth-twitter", "1.2.1"
|
|||
gem "twitter", "5.15.0"
|
||||
gem "omniauth-wordpress", "0.2.2"
|
||||
|
||||
# OpenID Connect
|
||||
gem "openid_connect", "0.8.3"
|
||||
|
||||
# Serializers
|
||||
|
||||
gem "active_model_serializers", "0.9.3"
|
||||
|
|
@ -192,6 +195,8 @@ gem "rubyzip", "1.1.7"
|
|||
# https://github.com/discourse/discourse/pull/238
|
||||
gem "minitest"
|
||||
|
||||
gem "versionist", "1.4.1"
|
||||
|
||||
# Windows and OSX have an execjs compatible runtime built-in, Linux users should
|
||||
# install Node.js or use "therubyracer".
|
||||
#
|
||||
|
|
@ -276,6 +281,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"
|
||||
|
|
|
|||
62
Gemfile.lock
62
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.8.3)
|
||||
activemodel
|
||||
attr_required (>= 0.0.5)
|
||||
json (>= 1.4.3)
|
||||
json-jwt (>= 0.5.5)
|
||||
rack-oauth2 (>= 1.0.0)
|
||||
swd (>= 0.1.2)
|
||||
tzinfo
|
||||
validate_email
|
||||
validate_url
|
||||
webfinger (>= 0.0.2)
|
||||
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,26 @@ 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
|
||||
versionist (1.4.1)
|
||||
activesupport (>= 3)
|
||||
railties (>= 3)
|
||||
yard (~> 0.7)
|
||||
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)
|
||||
|
|
@ -811,6 +868,7 @@ GEM
|
|||
xml-simple (1.1.5)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
yard (0.8.7.6)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
|
@ -830,6 +888,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 +926,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 +942,7 @@ DEPENDENCIES
|
|||
omniauth-twitter (= 1.2.1)
|
||||
omniauth-wordpress (= 0.2.2)
|
||||
open_graph_reader (= 0.6.1)
|
||||
openid_connect (= 0.8.3)
|
||||
pg (= 0.18.4)
|
||||
pronto (= 0.5.3)
|
||||
pronto-haml (= 0.5.0)
|
||||
|
|
@ -952,6 +1013,7 @@ DEPENDENCIES
|
|||
uglifier (= 2.7.2)
|
||||
unicorn (= 5.0.1)
|
||||
uuid (= 2.3.8)
|
||||
versionist (= 1.4.1)
|
||||
webmock (= 1.22.3)
|
||||
will_paginate (= 3.0.7)
|
||||
|
||||
|
|
|
|||
5
app/assets/javascripts/api/authorization_page.js
Normal file
5
app/assets/javascripts/api/authorization_page.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
$(document).ready(function() {
|
||||
$("#js-app-logo").error(function () {
|
||||
$(this).attr("src", ImagePaths.get("user/default.png"));
|
||||
});
|
||||
});
|
||||
|
|
@ -45,3 +45,4 @@
|
|||
//= require bootstrap-switch
|
||||
//= require blueimp-gallery
|
||||
//= require leaflet
|
||||
//= require api/authorization_page
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
@import 'perfect-scrollbar';
|
||||
|
||||
@import 'color-variables';
|
||||
@import "bootstrap-complete.scss";
|
||||
@import 'bootstrap-complete';
|
||||
@import 'mixins';
|
||||
|
||||
|
||||
|
|
@ -99,5 +99,11 @@
|
|||
@import 'statistics';
|
||||
|
||||
/* gallery */
|
||||
@import "blueimp-gallery";
|
||||
@import "gallery";
|
||||
@import 'blueimp-gallery';
|
||||
@import 'gallery';
|
||||
|
||||
// settings
|
||||
@import 'user_applications';
|
||||
|
||||
// OpenID Connect (API)
|
||||
@import 'openid_connect_error_page';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
@import "mobile/settings";
|
||||
@import "mobile/stream_element";
|
||||
@import "mobile/comments";
|
||||
@import 'mobile/openid_connect_error_page';
|
||||
|
||||
@import 'typography';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
.landing { margin: -56px -20px 10px; }
|
||||
|
||||
.api-error {
|
||||
background-color: $light-grey;
|
||||
box-shadow: $card-shadow;
|
||||
margin-top: 20px;
|
||||
|
||||
h4 { text-align: center; }
|
||||
}
|
||||
|
|
@ -30,11 +30,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
#blocked_people {
|
||||
.blocked_person {
|
||||
border-bottom: 1px solid $border-grey;
|
||||
margin-top: 0;
|
||||
.avatar { max-width: 35px; }
|
||||
.info { color: $text; }
|
||||
.applications-page .applications-explanation {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.application-img {
|
||||
margin: auto;
|
||||
max-width: 150px;
|
||||
text-align: center;
|
||||
|
||||
.entypo-browser {
|
||||
font-size: 137px;
|
||||
height: 160px;
|
||||
margin-top: -45px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
app/assets/stylesheets/openid_connect_error_page.scss
Normal file
7
app/assets/stylesheets/openid_connect_error_page.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.api-error {
|
||||
background-color: $light-grey;
|
||||
box-shadow: $card-shadow;
|
||||
margin-top: 20px;
|
||||
|
||||
h4 { text-align: center; }
|
||||
}
|
||||
35
app/assets/stylesheets/user_applications.scss
Normal file
35
app/assets/stylesheets/user_applications.scss
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.application-img {
|
||||
float: left;
|
||||
margin: 9px 0;
|
||||
max-height: 60px;
|
||||
text-align: center;
|
||||
width: 60px;
|
||||
|
||||
[class^="entypo-"] {
|
||||
font-size: 60px;
|
||||
height: 60px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
&::before {
|
||||
position: relative;
|
||||
top: -15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application-authorizations {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
padding: 0 0 15px 15px;
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.application-tos-policy > b {
|
||||
&:first-child { margin-right: 5px; }
|
||||
&:nth-child(2) { margin-left: 5px; }
|
||||
}
|
||||
|
||||
.user-consent { margin-top: 20px; }
|
||||
.approval-button { display: inline; }
|
||||
254
app/controllers/api/openid_connect/authorizations_controller.rb
Normal file
254
app/controllers/api/openid_connect/authorizations_controller.rb
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
class AuthorizationsController < ApplicationController
|
||||
rescue_from Rack::OAuth2::Server::Authorize::BadRequest do |e|
|
||||
logger.info e.backtrace[0, 10].join("\n")
|
||||
error, _description = e.message.split(" :: ")
|
||||
handle_params_error(error, "The request was malformed: please double check the client id and redirect uri.")
|
||||
end
|
||||
|
||||
rescue_from OpenSSL::SSL::SSLError do |e|
|
||||
logger.info e.backtrace[0, 10].join("\n")
|
||||
handle_params_error("bad_request", e.message)
|
||||
end
|
||||
|
||||
rescue_from JSON::JWS::VerificationFailed do |e|
|
||||
logger.info e.backtrace[0, 10].join("\n")
|
||||
handle_params_error("bad_request", e.message)
|
||||
end
|
||||
|
||||
before_action :auth_user_unless_prompt_none!
|
||||
|
||||
def new
|
||||
auth = Api::OpenidConnect::Authorization.find_by_client_id_and_user(params[:client_id], current_user)
|
||||
reset_auth(auth)
|
||||
if logged_in_before?(params[:max_age])
|
||||
reauthenticate(params)
|
||||
elsif params[:prompt]
|
||||
prompt = params[:prompt].split(" ")
|
||||
handle_prompt(prompt, auth)
|
||||
else
|
||||
handle_authorization_form(auth)
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
restore_request_parameters
|
||||
process_authorization_consent(params[:approve])
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorization = Api::OpenidConnect::Authorization.find_by(id: params[:id])
|
||||
if authorization
|
||||
authorization.destroy
|
||||
else
|
||||
flash[:error] = I18n.t("api.openid_connect.authorizations.destroy.fail", id: params[:id])
|
||||
end
|
||||
redirect_to api_openid_connect_user_applications_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_auth(auth)
|
||||
return unless auth
|
||||
auth.o_auth_access_tokens.destroy_all
|
||||
auth.id_tokens.destroy_all
|
||||
auth.code_used = false
|
||||
auth.save
|
||||
end
|
||||
|
||||
def handle_prompt(prompt, auth)
|
||||
if prompt.include? "select_account"
|
||||
handle_params_error("account_selection_required",
|
||||
"There is no support for choosing among multiple accounts")
|
||||
elsif prompt.include? "consent"
|
||||
request_authorization_consent_form
|
||||
else
|
||||
handle_authorization_form(auth)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_authorization_form(auth)
|
||||
if auth
|
||||
process_authorization_consent("true")
|
||||
else
|
||||
request_authorization_consent_form
|
||||
end
|
||||
end
|
||||
|
||||
def request_authorization_consent_form
|
||||
add_claims_to_scopes
|
||||
endpoint = Api::OpenidConnect::AuthorizationPoint::EndpointStartPoint.new(current_user)
|
||||
handle_start_point_response(endpoint)
|
||||
end
|
||||
|
||||
def add_claims_to_scopes
|
||||
return unless params[:claims]
|
||||
claims_json = JSON.parse(params[:claims])
|
||||
return unless claims_json
|
||||
claims_array = claims_json["userinfo"].try(:keys)
|
||||
return unless claims_array
|
||||
req = build_rack_request
|
||||
claims = claims_array.unshift(req[:scope]).join(" ")
|
||||
req.update_param("scope", claims)
|
||||
end
|
||||
|
||||
def logged_in_before?(seconds)
|
||||
if seconds.nil?
|
||||
false
|
||||
else
|
||||
(Time.now - current_user.current_sign_in_at) > seconds.to_i
|
||||
end
|
||||
end
|
||||
|
||||
def handle_start_point_response(endpoint)
|
||||
_status, header, response = endpoint.call(request.env)
|
||||
if response.redirect?
|
||||
redirect_to header["Location"]
|
||||
else
|
||||
save_params_and_render_consent_form(endpoint)
|
||||
end
|
||||
end
|
||||
|
||||
def save_params_and_render_consent_form(endpoint)
|
||||
@o_auth_application = endpoint.o_auth_application
|
||||
@response_type = endpoint.response_type
|
||||
@redirect_uri = endpoint.redirect_uri
|
||||
@scopes = endpoint.scopes
|
||||
save_request_parameters
|
||||
@app = UserApplicationPresenter.new @o_auth_application, @scopes
|
||||
render :new
|
||||
end
|
||||
|
||||
def save_request_parameters
|
||||
session[:client_id] = @o_auth_application.client_id
|
||||
session[:response_type] = @response_type
|
||||
session[:redirect_uri] = @redirect_uri
|
||||
session[:scopes] = scopes_as_space_seperated_values
|
||||
session[:nonce] = params[:nonce]
|
||||
end
|
||||
|
||||
def scopes_as_space_seperated_values
|
||||
@scopes.join(" ")
|
||||
end
|
||||
|
||||
def process_authorization_consent(approved_string)
|
||||
endpoint = Api::OpenidConnect::AuthorizationPoint::EndpointConfirmationPoint.new(
|
||||
current_user, to_boolean(approved_string))
|
||||
handle_confirmation_endpoint_response(endpoint)
|
||||
end
|
||||
|
||||
def handle_confirmation_endpoint_response(endpoint)
|
||||
_status, header, _response = endpoint.call(request.env)
|
||||
delete_authorization_session_variables
|
||||
redirect_to header["Location"]
|
||||
end
|
||||
|
||||
def delete_authorization_session_variables
|
||||
session.delete(:client_id)
|
||||
session.delete(:response_type)
|
||||
session.delete(:redirect_uri)
|
||||
session.delete(:scopes)
|
||||
session.delete(:nonce)
|
||||
end
|
||||
|
||||
def to_boolean(str)
|
||||
str.downcase == "true"
|
||||
end
|
||||
|
||||
def restore_request_parameters
|
||||
req = build_rack_request
|
||||
req.update_param("client_id", session[:client_id])
|
||||
req.update_param("redirect_uri", session[:redirect_uri])
|
||||
req.update_param("response_type", response_type_as_space_seperated_values)
|
||||
req.update_param("scope", session[:scopes])
|
||||
req.update_param("nonce", session[:nonce])
|
||||
end
|
||||
|
||||
def build_rack_request
|
||||
Rack::Request.new(request.env)
|
||||
end
|
||||
|
||||
def response_type_as_space_seperated_values
|
||||
[*session[:response_type]].join(" ")
|
||||
end
|
||||
|
||||
def handle_params_error(error, error_description)
|
||||
if params[:client_id] && params[:redirect_uri]
|
||||
handle_params_error_when_client_id_and_redirect_uri_exists(error, error_description)
|
||||
else
|
||||
render_error I18n.t("api.openid_connect.error_page.could_not_authorize"), error_description
|
||||
end
|
||||
end
|
||||
|
||||
def handle_params_error_when_client_id_and_redirect_uri_exists(error, error_description)
|
||||
app = Api::OpenidConnect::OAuthApplication.find_by(client_id: params[:client_id])
|
||||
if app && app.redirect_uris.include?(params[:redirect_uri])
|
||||
redirect_prompt_error_display(error, error_description)
|
||||
else
|
||||
render_error I18n.t("api.openid_connect.error_page.could_not_authorize"),
|
||||
"Invalid client id or redirect uri"
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_prompt_error_display(error, error_description)
|
||||
redirect_params_hash = {error: error, error_description: error_description, state: params[:state]}
|
||||
redirect_fragment = redirect_params_hash.compact.map {|key, value| key.to_s + "=" + value }.join("&")
|
||||
redirect_to params[:redirect_uri] + "?" + redirect_fragment
|
||||
end
|
||||
|
||||
def auth_user_unless_prompt_none!
|
||||
prompt = params[:prompt]
|
||||
if prompt && prompt.include?("none")
|
||||
handle_prompt_none
|
||||
elsif prompt && prompt.include?("login")
|
||||
new_params = params.merge!(prompt: prompt.remove("login"))
|
||||
reauthenticate(new_params)
|
||||
else
|
||||
authenticate_user!
|
||||
end
|
||||
end
|
||||
|
||||
def handle_prompt_none
|
||||
if params[:prompt] == "none"
|
||||
if user_signed_in?
|
||||
handle_prompt_with_signed_in_user
|
||||
else
|
||||
handle_params_error("login_required", "User must already be logged in when `prompt` is `none`")
|
||||
end
|
||||
else
|
||||
handle_params_error("invalid_request", "The 'none' value cannot be used with any other prompt value")
|
||||
end
|
||||
end
|
||||
|
||||
def handle_prompt_with_signed_in_user
|
||||
client_id = params[:client_id]
|
||||
if client_id
|
||||
auth = Api::OpenidConnect::Authorization.find_by_client_id_and_user(client_id, current_user)
|
||||
if auth
|
||||
process_authorization_consent("true")
|
||||
else
|
||||
handle_params_error("interaction_required", "User must already be authorized when `prompt` is `none`")
|
||||
end
|
||||
else
|
||||
handle_params_error("bad_request", "Client ID is missing from request")
|
||||
end
|
||||
end
|
||||
|
||||
def reauthenticate(params)
|
||||
sign_out current_user
|
||||
redirect_to new_api_openid_connect_authorization_path(params)
|
||||
end
|
||||
|
||||
def render_error(error_description, detailed_error=nil)
|
||||
@error_description = error_description
|
||||
@detailed_error = detailed_error
|
||||
if request.format == :mobile
|
||||
render "api/openid_connect/error/error.mobile", layout: "application.mobile"
|
||||
else
|
||||
render "api/openid_connect/error/error", layout: "with_header_with_footer"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
52
app/controllers/api/openid_connect/clients_controller.rb
Normal file
52
app/controllers/api/openid_connect/clients_controller.rb
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
class ClientsController < ApplicationController
|
||||
rescue_from OpenIDConnect::HttpError do |e|
|
||||
http_error_page_as_json(e)
|
||||
end
|
||||
|
||||
rescue_from OpenIDConnect::ValidationFailed,
|
||||
ActiveRecord::RecordInvalid, Api::OpenidConnect::Error::InvalidSectorIdentifierUri do |e|
|
||||
validation_fail_as_json(e)
|
||||
end
|
||||
|
||||
rescue_from Api::OpenidConnect::Error::InvalidRedirectUri do |e|
|
||||
validation_fail_redirect_uri(e)
|
||||
end
|
||||
|
||||
rescue_from OpenSSL::SSL::SSLError do |e|
|
||||
validation_fail_as_json(e)
|
||||
end
|
||||
|
||||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/app/controllers/connect/clients_controller.rb#L24
|
||||
def create
|
||||
registrar = OpenIDConnect::Client::Registrar.new(request.url, params)
|
||||
client = Api::OpenidConnect::OAuthApplication.register! registrar
|
||||
render json: client.as_json(root: false)
|
||||
end
|
||||
|
||||
def find
|
||||
client = Api::OpenidConnect::OAuthApplication.find_by(client_name: params[:client_name])
|
||||
if client
|
||||
render json: {client_id: client.client_id}
|
||||
else
|
||||
render json: {error: "Client with name #{params[:client_name]} does not exist"}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def http_error_page_as_json(e)
|
||||
render json: {error: :invalid_request, error_description: e.message}, status: 400
|
||||
end
|
||||
|
||||
def validation_fail_as_json(e)
|
||||
render json: {error: :invalid_client_metadata, error_description: e.message}, status: 400
|
||||
end
|
||||
|
||||
def validation_fail_redirect_uri(e)
|
||||
render json: {error: :invalid_redirect_uri, error_description: e.message}, status: 400
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
app/controllers/api/openid_connect/discovery_controller.rb
Normal file
61
app/controllers/api/openid_connect/discovery_controller.rb
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Copyright (c) 2011 nov matake
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# See https://github.com/nov/openid_connect_sample/blob/master/app/controllers/discovery_controller.rb
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class DiscoveryController < ApplicationController
|
||||
def webfinger
|
||||
jrd = {
|
||||
links: [{
|
||||
rel: OpenIDConnect::Discovery::Provider::Issuer::REL_VALUE,
|
||||
href: root_url
|
||||
}]
|
||||
}
|
||||
jrd[:subject] = params[:resource] if params[:resource].present?
|
||||
render json: jrd, content_type: "application/jrd+json"
|
||||
end
|
||||
|
||||
def configuration
|
||||
render json: OpenIDConnect::Discovery::Provider::Config::Response.new(
|
||||
issuer: root_url,
|
||||
registration_endpoint: api_openid_connect_clients_url,
|
||||
authorization_endpoint: new_api_openid_connect_authorization_url,
|
||||
token_endpoint: api_openid_connect_access_tokens_url,
|
||||
userinfo_endpoint: api_openid_connect_user_info_url,
|
||||
jwks_uri: api_openid_connect_url,
|
||||
scopes_supported: Api::OpenidConnect::Authorization::SCOPES,
|
||||
response_types_supported: Api::OpenidConnect::OAuthApplication.available_response_types,
|
||||
request_object_signing_alg_values_supported: %i(none),
|
||||
request_parameter_supported: true,
|
||||
request_uri_parameter_supported: true,
|
||||
subject_types_supported: %w(public pairwise),
|
||||
id_token_signing_alg_values_supported: %i(RS256),
|
||||
token_endpoint_auth_methods_supported: %w(client_secret_basic client_secret_post private_key_jwt),
|
||||
claims_parameter_supported: true,
|
||||
claims_supported: %w(sub name nickname profile picture),
|
||||
userinfo_signing_alg_values_supported: %w(none)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
36
app/controllers/api/openid_connect/id_tokens_controller.rb
Normal file
36
app/controllers/api/openid_connect/id_tokens_controller.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Copyright (c) 2011 nov matake
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class IdTokensController < ApplicationController
|
||||
def jwks
|
||||
render json: JSON::JWK::Set.new(build_jwk).as_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_jwk
|
||||
JSON::JWK.new(Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY, use: :sig, kid: :default)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
class TokenEndpointController < ApplicationController
|
||||
def create
|
||||
req = Rack::Request.new(request.env)
|
||||
if req["client_assertion_type"] == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
||||
handle_jwt_bearer(req)
|
||||
end
|
||||
self.status, response.headers, self.response_body = Api::OpenidConnect::TokenEndpoint.new.call(request.env)
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_jwt_bearer(req)
|
||||
jwt_string = req["client_assertion"]
|
||||
jwt = JSON::JWT.decode jwt_string, :skip_verification
|
||||
o_auth_app = Api::OpenidConnect::OAuthApplication.find_by(client_id: jwt["iss"])
|
||||
raise Rack::OAuth2::Server::Authorize::BadRequest(:invalid_request) unless o_auth_app
|
||||
public_key = fetch_public_key(o_auth_app, jwt)
|
||||
JSON::JWT.decode(jwt_string, JSON::JWK.new(public_key).to_key)
|
||||
req.update_param("client_id", o_auth_app.client_id)
|
||||
req.update_param("client_secret", o_auth_app.client_secret)
|
||||
end
|
||||
|
||||
def fetch_public_key(o_auth_app, jwt)
|
||||
public_key = fetch_public_key_from_json(o_auth_app.jwks, jwt)
|
||||
if public_key.empty? && o_auth_app.jwks_uri
|
||||
response = Faraday.get(o_auth_app.jwks_uri)
|
||||
public_key = fetch_public_key_from_json(response.body, jwt)
|
||||
end
|
||||
raise Rack::OAuth2::Server::Authorize::BadRequest(:unauthorized_client) if public_key.empty?
|
||||
public_key
|
||||
end
|
||||
|
||||
def fetch_public_key_from_json(string, jwt)
|
||||
json = JSON.parse(string)
|
||||
keys = json["keys"]
|
||||
public_key = get_key_from_kid(keys, jwt.header["kid"])
|
||||
public_key
|
||||
end
|
||||
|
||||
def get_key_from_kid(keys, kid)
|
||||
keys.each do |key|
|
||||
return key if key.has_value?(kid)
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from Rack::OAuth2::Server::Authorize::BadRequest,
|
||||
JSON::JWT::InvalidFormat, JSON::JWK::UnknownAlgorithm do |e|
|
||||
logger.info e.backtrace[0, 10].join("\n")
|
||||
render json: {error: :invalid_request, error_description: e.message, status: 400}
|
||||
end
|
||||
rescue_from JSON::JWT::VerificationFailed do |e|
|
||||
logger.info e.backtrace[0, 10].join("\n")
|
||||
render json: {error: :invalid_grant, error_description: e.message, status: 400}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
class UserApplicationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@user_apps = UserApplicationsPresenter.new current_user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
26
app/controllers/api/openid_connect/user_info_controller.rb
Normal file
26
app/controllers/api/openid_connect/user_info_controller.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
class UserInfoController < ApplicationController
|
||||
include Api::OpenidConnect::ProtectedResourceEndpoint
|
||||
|
||||
before_action do
|
||||
require_access_token %w(openid)
|
||||
end
|
||||
|
||||
def show
|
||||
serializer = UserInfoSerializer.new(current_user)
|
||||
auth = current_token.authorization
|
||||
serializer.serialization_options = {authorization: auth}
|
||||
attributes_without_essential =
|
||||
serializer.attributes.with_indifferent_access.select {|scope| auth.scopes.include? scope }
|
||||
attributes = attributes_without_essential.merge(
|
||||
sub: serializer.sub)
|
||||
render json: attributes.to_json
|
||||
end
|
||||
|
||||
def current_user
|
||||
current_token ? current_token.authorization.user : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/controllers/api/v0/base_controller.rb
Normal file
13
app/controllers/api/v0/base_controller.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module Api
|
||||
module V0
|
||||
class BaseController < ApplicationController
|
||||
include Api::OpenidConnect::ProtectedResourceEndpoint
|
||||
|
||||
protected
|
||||
|
||||
def current_user
|
||||
current_token ? current_token.authorization.user : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# the COPYRIGHT file.
|
||||
|
||||
class UsersController < ApplicationController
|
||||
before_action :authenticate_user!, :except => [:new, :create, :public, :user_photo]
|
||||
before_action :authenticate_user!, except: %i(new create public user_photo)
|
||||
respond_to :html
|
||||
|
||||
def edit
|
||||
|
|
|
|||
9
app/helpers/user_applications_helper.rb
Normal file
9
app/helpers/user_applications_helper.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
module UserApplicationsHelper
|
||||
def user_application_name(app)
|
||||
if app.name?
|
||||
"#{html_escape app.name} (#{link_to(app.url, app.url)})"
|
||||
else
|
||||
link_to(app.url, app.url)
|
||||
end
|
||||
end
|
||||
end
|
||||
80
app/models/api/openid_connect/authorization.rb
Normal file
80
app/models/api/openid_connect/authorization.rb
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/app/models/authorization.rb
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class Authorization < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :o_auth_application
|
||||
|
||||
validates :user, presence: true, uniqueness: {scope: :o_auth_application}
|
||||
validates :o_auth_application, presence: true
|
||||
validate :validate_scope_names
|
||||
serialize :scopes, JSON
|
||||
|
||||
has_many :o_auth_access_tokens, dependent: :destroy
|
||||
has_many :id_tokens, dependent: :destroy
|
||||
|
||||
before_validation :setup, on: :create
|
||||
|
||||
scope :with_redirect_uri, ->(given_uri) { where redirect_uri: given_uri }
|
||||
|
||||
SCOPES = %w(openid sub aud name nickname profile picture read write)
|
||||
|
||||
def setup
|
||||
self.refresh_token = SecureRandom.hex(32)
|
||||
end
|
||||
|
||||
def validate_scope_names
|
||||
return unless scopes
|
||||
scopes.each do |scope|
|
||||
errors.add(:scope, "is not a valid scope name") unless SCOPES.include? scope
|
||||
end
|
||||
end
|
||||
|
||||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/app/models/access_token.rb#L26
|
||||
def accessible?(required_scopes=nil)
|
||||
Array(required_scopes).all? { |required_scope|
|
||||
scopes.include? required_scope
|
||||
}
|
||||
end
|
||||
|
||||
def create_code
|
||||
SecureRandom.hex(32).tap do |code|
|
||||
update!(code: code)
|
||||
update!(code_used: false)
|
||||
end
|
||||
end
|
||||
|
||||
def create_access_token
|
||||
o_auth_access_tokens.create!.bearer_token
|
||||
end
|
||||
|
||||
def create_id_token
|
||||
id_tokens.create!(nonce: nonce)
|
||||
end
|
||||
|
||||
def self.find_by_client_id_and_user(client_id, user)
|
||||
app = Api::OpenidConnect::OAuthApplication.where(client_id: client_id)
|
||||
find_by(o_auth_application: app, user: user)
|
||||
end
|
||||
|
||||
def self.find_by_refresh_token(client_id, refresh_token)
|
||||
app = Api::OpenidConnect::OAuthApplication.where(client_id: client_id)
|
||||
find_by(o_auth_application: app, refresh_token: refresh_token)
|
||||
end
|
||||
|
||||
def self.use_code(code)
|
||||
return unless code
|
||||
auth = find_by(code: code)
|
||||
return unless auth
|
||||
if auth.code_used
|
||||
auth.destroy
|
||||
nil
|
||||
else
|
||||
auth.update!(code_used: true)
|
||||
auth
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
71
app/models/api/openid_connect/id_token.rb
Normal file
71
app/models/api/openid_connect/id_token.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Copyright (c) 2011 nov matake
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# See https://github.com/nov/openid_connect_sample/blob/master/app/models/id_token.rb
|
||||
|
||||
require "uri"
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class IdToken < ActiveRecord::Base
|
||||
belongs_to :authorization
|
||||
|
||||
before_validation :setup, on: :create
|
||||
|
||||
default_scope { where("expires_at >= ?", Time.zone.now.utc) }
|
||||
|
||||
def setup
|
||||
self.expires_at = 30.minutes.from_now
|
||||
end
|
||||
|
||||
def to_jwt(options={})
|
||||
to_response_object(options).to_jwt(OpenidConnect::IdTokenConfig::PRIVATE_KEY) do |jwt|
|
||||
jwt.kid = :default
|
||||
end
|
||||
end
|
||||
|
||||
def to_response_object(options={})
|
||||
OpenIDConnect::ResponseObject::IdToken.new(claims).tap do |id_token|
|
||||
id_token.code = options[:code] if options[:code]
|
||||
id_token.access_token = options[:access_token] if options[:access_token]
|
||||
end
|
||||
end
|
||||
|
||||
def claims
|
||||
sub = build_sub
|
||||
@claims ||= {
|
||||
iss: AppConfig.environment.url,
|
||||
sub: sub,
|
||||
aud: authorization.o_auth_application.client_id,
|
||||
exp: expires_at.to_i,
|
||||
iat: created_at.to_i,
|
||||
auth_time: authorization.user.current_sign_in_at.to_i,
|
||||
nonce: nonce,
|
||||
acr: 0
|
||||
}
|
||||
end
|
||||
|
||||
def build_sub
|
||||
Api::OpenidConnect::SubjectIdentifierCreator.create(authorization)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/models/api/openid_connect/o_auth_access_token.rb
Normal file
49
app/models/api/openid_connect/o_auth_access_token.rb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright (c) 2011 nov matake
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# See https://github.com/nov/openid_connect_sample/blob/master/app/models/access_token.rb
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class OAuthAccessToken < ActiveRecord::Base
|
||||
belongs_to :authorization
|
||||
|
||||
before_validation :setup, on: :create
|
||||
|
||||
validates :token, presence: true, uniqueness: true
|
||||
validates :authorization, presence: true
|
||||
|
||||
scope :valid, ->(time) { where("expires_at >= ?", time) }
|
||||
|
||||
def setup
|
||||
self.token = SecureRandom.hex(32)
|
||||
self.expires_at = 24.hours.from_now
|
||||
end
|
||||
|
||||
def bearer_token
|
||||
@bearer_token ||= Rack::OAuth2::AccessToken::Bearer.new(
|
||||
access_token: token,
|
||||
expires_in: (expires_at - Time.zone.now.utc).to_i
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
119
app/models/api/openid_connect/o_auth_application.rb
Normal file
119
app/models/api/openid_connect/o_auth_application.rb
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# Copyright (c) 2011 nov matake
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# See https://github.com/nov/openid_connect_sample/blob/master/app/models/client.rb
|
||||
|
||||
require "digest"
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class OAuthApplication < ActiveRecord::Base
|
||||
has_many :authorizations, dependent: :destroy
|
||||
has_many :user, through: :authorizations
|
||||
|
||||
validates :client_id, presence: true, uniqueness: true
|
||||
validates :client_secret, presence: true
|
||||
validates :client_name, uniqueness: {scope: :redirect_uris}
|
||||
|
||||
%i(redirect_uris response_types grant_types contacts jwks).each do |serializable|
|
||||
serialize serializable, JSON
|
||||
end
|
||||
|
||||
before_validation :setup, on: :create
|
||||
before_validation do
|
||||
redirect_uris.sort!
|
||||
end
|
||||
|
||||
def setup
|
||||
self.client_id = SecureRandom.hex(16)
|
||||
self.client_secret = SecureRandom.hex(32)
|
||||
end
|
||||
|
||||
def image_uri
|
||||
logo_uri ? Diaspora::Camo.image_url(logo_uri) : nil
|
||||
end
|
||||
|
||||
class << self
|
||||
def available_response_types
|
||||
["id_token", "id_token token", "code"]
|
||||
end
|
||||
|
||||
def register!(registrar)
|
||||
registrar.validate!
|
||||
build_client_application(registrar)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_client_application(registrar)
|
||||
attributes = registrar_attributes(registrar)
|
||||
check_sector_identifier_uri(attributes)
|
||||
check_redirect_uris(attributes)
|
||||
create! attributes
|
||||
end
|
||||
|
||||
def check_sector_identifier_uri(attributes)
|
||||
sector_identifier_uri = attributes[:sector_identifier_uri]
|
||||
return unless sector_identifier_uri
|
||||
response = Faraday.get(sector_identifier_uri)
|
||||
sector_identifier_uri_json = JSON.parse(response.body)
|
||||
redirect_uris = attributes[:redirect_uris]
|
||||
sector_identifier_uri_includes_redirect_uris = (redirect_uris - sector_identifier_uri_json).empty?
|
||||
return if sector_identifier_uri_includes_redirect_uris
|
||||
raise Api::OpenidConnect::Error::InvalidSectorIdentifierUri.new
|
||||
end
|
||||
|
||||
def check_redirect_uris(attributes)
|
||||
redirect_uris = attributes[:redirect_uris]
|
||||
uri_array = redirect_uris.map {|uri| URI(uri) }
|
||||
any_uri_contains_fragment = uri_array.any? {|uri| !uri.fragment.nil? }
|
||||
return unless any_uri_contains_fragment
|
||||
raise Api::OpenidConnect::Error::InvalidRedirectUri.new
|
||||
end
|
||||
|
||||
def supported_metadata
|
||||
%i(client_name response_types grant_types application_type
|
||||
contacts logo_uri client_uri policy_uri tos_uri redirect_uris
|
||||
sector_identifier_uri subject_type token_endpoint_auth_method jwks jwks_uri)
|
||||
end
|
||||
|
||||
def registrar_attributes(registrar)
|
||||
supported_metadata.each_with_object({}) do |key, attr|
|
||||
value = registrar.public_send(key)
|
||||
next unless value
|
||||
case key
|
||||
when :subject_type
|
||||
attr[:ppid] = (value == "pairwise")
|
||||
when :jwks_uri
|
||||
response = Faraday.get(value)
|
||||
attr[:jwks] = response.body
|
||||
attr[:jwks_uri] = value
|
||||
when :jwks
|
||||
attr[:jwks] = value.to_json
|
||||
else
|
||||
attr[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright (c) 2011 nov matake
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# See https://github.com/nov/openid_connect_sample/blob/master/app/models/pairwise_pseudonymous_identifier.rb
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class PairwisePseudonymousIdentifier < ActiveRecord::Base
|
||||
self.table_name = "ppid"
|
||||
|
||||
belongs_to :o_auth_application
|
||||
belongs_to :user
|
||||
|
||||
validates :user, presence: true
|
||||
validates :identifier, presence: true, uniqueness: {scope: :user}
|
||||
validates :guid, presence: true, uniqueness: true
|
||||
|
||||
before_validation :setup, on: :create
|
||||
|
||||
private
|
||||
|
||||
def setup
|
||||
self.guid = SecureRandom.hex(16)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -76,6 +76,10 @@ class User < ActiveRecord::Base
|
|||
|
||||
has_many :reports
|
||||
|
||||
has_many :pairwise_pseudonymous_identifiers, class_name: "Api::OpenidConnect::PairwisePseudonymousIdentifier"
|
||||
has_many :authorizations, class_name: "Api::OpenidConnect::Authorization"
|
||||
has_many :o_auth_applications, through: :authorizations, class_name: "Api::OpenidConnect::OAuthApplication"
|
||||
|
||||
before_save :guard_unconfirmed_email,
|
||||
:save_person!
|
||||
|
||||
|
|
@ -601,12 +605,10 @@ class User < ActiveRecord::Base
|
|||
private
|
||||
|
||||
def clearable_fields
|
||||
self.attributes.keys - ["id", "username", "encrypted_password",
|
||||
"created_at", "updated_at", "locked_at",
|
||||
"serialized_private_key", "getting_started",
|
||||
"disable_mail", "show_community_spotlight_in_stream",
|
||||
"strip_exif", "email", "remove_after",
|
||||
"export", "exporting", "exported_at",
|
||||
"exported_photos_file", "exporting_photos", "exported_photos_at"]
|
||||
attributes.keys - %w(id username encrypted_password created_at updated_at locked_at
|
||||
serialized_private_key getting_started
|
||||
disable_mail show_community_spotlight_in_stream
|
||||
strip_exif email remove_after export exporting exported_at
|
||||
exported_photos_file exporting_photos exported_photos_at)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
47
app/presenters/user_application_presenter.rb
Normal file
47
app/presenters/user_application_presenter.rb
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
class UserApplicationPresenter
|
||||
attr_reader :scopes
|
||||
|
||||
def initialize(application, scopes, authorization_id=nil)
|
||||
@app = application
|
||||
@scopes = scopes
|
||||
@authorization_id = authorization_id
|
||||
end
|
||||
|
||||
def id
|
||||
@authorization_id
|
||||
end
|
||||
|
||||
def name
|
||||
@app.client_name
|
||||
end
|
||||
|
||||
def image
|
||||
@app.image_uri
|
||||
end
|
||||
|
||||
def terms_of_services
|
||||
@app.tos_uri
|
||||
end
|
||||
|
||||
def policy
|
||||
@app.policy_uri
|
||||
end
|
||||
|
||||
def name?
|
||||
@app.client_name.present?
|
||||
end
|
||||
|
||||
def terms_of_services?
|
||||
@app.tos_uri.present?
|
||||
end
|
||||
|
||||
def policy?
|
||||
@app.policy_uri.present?
|
||||
end
|
||||
|
||||
def url
|
||||
client_redirect = URI(@app.redirect_uris[0])
|
||||
client_redirect.path = "/"
|
||||
client_redirect.to_s
|
||||
end
|
||||
end
|
||||
20
app/presenters/user_applications_presenter.rb
Normal file
20
app/presenters/user_applications_presenter.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
class UserApplicationsPresenter
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def user_applications
|
||||
@applications ||= @user.o_auth_applications.map do |app|
|
||||
authorization = Api::OpenidConnect::Authorization.find_by_client_id_and_user(app.client_id, @user)
|
||||
UserApplicationPresenter.new app, authorization.scopes, authorization.id
|
||||
end
|
||||
end
|
||||
|
||||
def applications_count
|
||||
user_applications.size
|
||||
end
|
||||
|
||||
def applications?
|
||||
applications_count > 0
|
||||
end
|
||||
end
|
||||
24
app/serializers/user_info_serializer.rb
Normal file
24
app/serializers/user_info_serializer.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
class UserInfoSerializer < ActiveModel::Serializer
|
||||
attributes :sub, :name, :nickname, :profile, :picture
|
||||
|
||||
def sub
|
||||
auth = serialization_options[:authorization]
|
||||
Api::OpenidConnect::SubjectIdentifierCreator.create(auth)
|
||||
end
|
||||
|
||||
def name
|
||||
(object.first_name || "") + (object.last_name || "")
|
||||
end
|
||||
|
||||
def nickname
|
||||
object.name
|
||||
end
|
||||
|
||||
def profile
|
||||
File.join(AppConfig.environment.url, "people", object.guid).to_s
|
||||
end
|
||||
|
||||
def picture
|
||||
File.join(AppConfig.environment.url, object.image_url).to_s
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
.application-img
|
||||
- if app.image
|
||||
= image_tag app.image, class: "img-responsive", id: "js-app-logo"
|
||||
- else
|
||||
%i.entypo-browser
|
||||
.application-authorizations
|
||||
- if app.scopes.count > 0
|
||||
%h4
|
||||
= t("api.openid_connect.authorizations.new.access", name: user_application_name(app)).html_safe
|
||||
%ul
|
||||
- app.scopes.each do |scope|
|
||||
%li
|
||||
%strong= t("api.openid_connect.scopes.#{scope}.name")
|
||||
%p= t("api.openid_connect.scopes.#{scope}.description")
|
||||
- else
|
||||
.well
|
||||
= t("api.openid_connect.authorizations.new.no_requirement", name: user_application_name(app)).html_safe
|
||||
|
||||
.small-horizontal-spacer
|
||||
.application-tos-policy
|
||||
- if app.terms_of_services?
|
||||
%strong= link_to t("api.openid_connect.user_applications.tos"), app.terms_of_services
|
||||
|
||||
- if app.policy? && app.terms_of_services?
|
||||
|
|
||||
|
||||
- if app.policy?
|
||||
%strong= link_to t("api.openid_connect.user_applications.policy"), app.policy
|
||||
|
||||
- if app.policy? || app.terms_of_services?
|
||||
.small-horizontal-spacer
|
||||
13
app/views/api/openid_connect/authorizations/new.html.haml
Normal file
13
app/views/api/openid_connect/authorizations/new.html.haml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.user-consent.col-md-10.col-md-offset-1
|
||||
%ul.list-group
|
||||
%li.list-group-item.authorized-application.clearfix
|
||||
= render "grants_list", app: @app
|
||||
|
||||
.clearfix.pull-right
|
||||
= form_tag api_openid_connect_authorizations_path, class: "approval-button" do
|
||||
= submit_tag t(".deny"), class: "btn btn-danger"
|
||||
= hidden_field_tag :approve, false
|
||||
|
||||
= form_tag api_openid_connect_authorizations_path, class: "approval-button"do
|
||||
= submit_tag t(".approve"), class: "btn btn-primary"
|
||||
= hidden_field_tag :approve, true
|
||||
11
app/views/api/openid_connect/error/_error.html.haml
Normal file
11
app/views/api/openid_connect/error/_error.html.haml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.container-fluid
|
||||
.row
|
||||
.api-error.col-sm-6.col-sm-offset-3
|
||||
%h4
|
||||
%b= t("api.openid_connect.error_page.title")
|
||||
%div{id: "openid_connect_error_description"}
|
||||
%p= @error_description
|
||||
- unless @detailed_error.nil?
|
||||
%p= t("api.openid_connect.error_page.contact_developer")
|
||||
%pre= @detailed_error
|
||||
|
||||
1
app/views/api/openid_connect/error/error.html.haml
Normal file
1
app/views/api/openid_connect/error/error.html.haml
Normal file
|
|
@ -0,0 +1 @@
|
|||
= render partial: "api/openid_connect/error/error"
|
||||
8
app/views/api/openid_connect/error/error.mobile.haml
Normal file
8
app/views/api/openid_connect/error/error.mobile.haml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.landing
|
||||
%h1.session
|
||||
= pod_name
|
||||
|
||||
= render partial: "api/openid_connect/error/error"
|
||||
|
||||
%footer
|
||||
= link_to t("layouts.application.toggle"), toggle_mobile_path
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
- if @user_apps.applications?
|
||||
%ul.list-group
|
||||
- @user_apps.user_applications.each do |app|
|
||||
%li.list-group-item.authorized-application
|
||||
= render "grants_list", app: app
|
||||
= form_for "application", url: "#{api_openid_connect_authorization_path(app.id)}",
|
||||
html: {method: :delete, class: "form-horizontal"} do |f|
|
||||
.clearfix= f.submit t("api.openid_connect.user_applications.revoke_autorization"),
|
||||
class: "btn btn-danger pull-right app-revoke"
|
||||
|
||||
- else
|
||||
.well
|
||||
%h4
|
||||
= t("api.openid_connect.user_applications.no_applications")
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
.application-img
|
||||
- if app.image
|
||||
= image_tag app.image, class: "img-responsive", id: "js-app-logo"
|
||||
- else
|
||||
%i.entypo-browser
|
||||
.application-authorizations
|
||||
- if app.scopes.count > 0
|
||||
%h4= t("api.openid_connect.user_applications.index.access", name: user_application_name(app)).html_safe
|
||||
%ul
|
||||
- app.scopes.each do |scope|
|
||||
%li
|
||||
%b= t("api.openid_connect.scopes.#{scope}.name")
|
||||
%p= t("api.openid_connect.scopes.#{scope}.description")
|
||||
- else
|
||||
.well
|
||||
= t("api.openid_connect.user_applications.index.no_requirement", name: user_application_name(app)).html_safe
|
||||
|
||||
.small-horizontal-spacer
|
||||
.application-tos-policy
|
||||
- if app.terms_of_services?
|
||||
%b= link_to t("api.openid_connect.user_applications.tos"), app.terms_of_services
|
||||
|
||||
- if app.policy? && app.terms_of_services?
|
||||
|
|
||||
|
||||
- if app.policy?
|
||||
%b= link_to t("api.openid_connect.user_applications.policy"), app.policy
|
||||
|
||||
- if app.policy? || app.terms_of_services?
|
||||
.small-horizontal-spacer
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
- content_for :page_title do
|
||||
= t(".edit_applications")
|
||||
|
||||
.container-fluid.applications-page
|
||||
.row
|
||||
.col-lg-10.col-lg-offset-1
|
||||
= render "shared/settings_nav"
|
||||
.row
|
||||
.col-lg-8.col-lg-offset-2
|
||||
%h3= t(".title")
|
||||
.row
|
||||
.col-md-12
|
||||
= render "add_remove_applications"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.container-fluid.settings_container.applications-page
|
||||
.row
|
||||
.col-lg-10.col-lg-offset-1
|
||||
- content_for :page_title do
|
||||
= t(".edit_applications")
|
||||
= render "shared/settings_nav"
|
||||
.row
|
||||
.col-md-12
|
||||
= render "add_remove_applications"
|
||||
|
|
@ -6,3 +6,5 @@
|
|||
%li{class: current_page?(edit_user_path) && "active"}= link_to t("account"), edit_user_path
|
||||
%li{class: current_page?(privacy_settings_path) && "active"}= link_to t("privacy"), privacy_settings_path
|
||||
%li{class: current_page?(services_path) && "active"}= link_to t("_services"), services_path
|
||||
%li{class: current_page?(api_openid_connect_user_applications_path) && "active"}
|
||||
= link_to t("_applications"), api_openid_connect_user_applications_path
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
#settings_nav
|
||||
%h2= t('settings')
|
||||
%h2= t("settings")
|
||||
%nav
|
||||
%ul
|
||||
%li= link_to_unless_current t('profile'), edit_profile_path
|
||||
%li= link_to_unless_current t('account'), edit_user_path
|
||||
%li= link_to_unless_current t('privacy'), privacy_settings_path
|
||||
%li= link_to_unless_current t('_services'), services_path
|
||||
%li= link_to_unless_current t("profile"), edit_profile_path
|
||||
%li= link_to_unless_current t("account"), edit_user_path
|
||||
%li= link_to_unless_current t("privacy"), privacy_settings_path
|
||||
%li= link_to_unless_current t("_services"), services_path
|
||||
%li= link_to_unless_current t("_applications"), api_openid_connect_user_applications_path
|
||||
|
|
|
|||
|
|
@ -107,5 +107,10 @@ 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|
|
||||
Api::OpenidConnect::OAuthAccessToken
|
||||
.valid(Time.zone.now.utc).find_by(token: req.access_token) || req.invalid_token!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
Rails.application.config.middleware.insert 0, Rack::Cors do
|
||||
allow do
|
||||
origins '*'
|
||||
resource '/.well-known/host-meta'
|
||||
resource '/webfinger'
|
||||
origins "*"
|
||||
resource "/api/openid_connect/user_info", methods: %i(get post)
|
||||
resource "/api/v0/*", methods: %i(delete get post)
|
||||
resource "/.well-known/host-meta"
|
||||
resource "/.well-known/webfinger"
|
||||
resource "/.well-known/openid-configuration"
|
||||
resource "/webfinger"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -878,6 +878,48 @@ en:
|
|||
Hoping to see you again,
|
||||
|
||||
The diaspora* email robot!
|
||||
api:
|
||||
openid_connect:
|
||||
authorizations:
|
||||
new:
|
||||
redirection_message: "Are you sure you want to give access to %{redirect_uri}?"
|
||||
access: "%{name} requires access to:"
|
||||
no_requirement: "%{name} requires no permissions"
|
||||
approve: "Approve"
|
||||
deny: "Deny"
|
||||
bad_request: "Missing client id or redirect URI"
|
||||
client_id_not_found: "No client with client_id %{client_id} with redirect URI %{redirect_uri} found"
|
||||
destroy:
|
||||
fail: "The attempt to revoke the authorization with ID %{id} failed"
|
||||
user_applications:
|
||||
index:
|
||||
edit_applications: "Applications"
|
||||
title: "Authorized applications"
|
||||
access: "%{name} has access to:"
|
||||
no_requirement: "%{name} requires no permissions"
|
||||
no_applications: "You have no authorized applications"
|
||||
revoke_autorization: "Revoke"
|
||||
tos: "See the application's terms of service"
|
||||
policy: "See the application's privacy policy"
|
||||
scopes:
|
||||
openid:
|
||||
name: "basic profile"
|
||||
description: "This allows the application to read your basic profile"
|
||||
extended:
|
||||
name: "extended profile"
|
||||
description: "This allows the application to read your extended profile"
|
||||
read:
|
||||
name: "read profile, stream and conversations"
|
||||
description: "This allows the application to read your stream, your conversations and your complete profile"
|
||||
write:
|
||||
name: "send posts, conversations and reactions"
|
||||
description: "This allows the application to send new posts, write conversations, and send reactions"
|
||||
error_page:
|
||||
title: "Oh! Something went wrong :("
|
||||
contact_developer: "You should contact the developer of the application and include the following detailed error message:"
|
||||
login_required: "You must first login before you can authorize this application"
|
||||
could_not_authorize: "The application could not be authorized"
|
||||
|
||||
people:
|
||||
zero: "No people"
|
||||
one: "1 person"
|
||||
|
|
|
|||
|
|
@ -198,15 +198,7 @@ Diaspora::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
scope 'api/v0', :controller => :apis do
|
||||
get :me
|
||||
end
|
||||
|
||||
namespace :api do
|
||||
namespace :v0 do
|
||||
get "/users/:username" => 'users#show', :as => 'user'
|
||||
get "/tags/:name" => 'tags#show', :as => 'tag'
|
||||
end
|
||||
namespace :v1 do
|
||||
resources :tokens, :only => [:create, :destroy]
|
||||
end
|
||||
|
|
@ -240,4 +232,28 @@ Diaspora::Application.routes.draw do
|
|||
|
||||
# Startpage
|
||||
root :to => 'home#show'
|
||||
|
||||
api_version(module: "Api::V0", path: {value: "api/v0"}, default: true) do
|
||||
match "user", to: "users#show", via: %i(get post)
|
||||
end
|
||||
|
||||
namespace :api do
|
||||
namespace :openid_connect do
|
||||
resources :clients, only: :create
|
||||
get "clients/find", to: "clients#find"
|
||||
|
||||
post "access_tokens", to: "token_endpoint#create"
|
||||
|
||||
# Authorization Servers MUST support the use of the HTTP GET and POST methods at the Authorization Endpoint
|
||||
# See http://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation
|
||||
resources :authorizations, only: %i(new create destroy)
|
||||
post "authorizations/new", to: "authorizations#new"
|
||||
get "user_applications", to: "user_applications#index"
|
||||
get "jwks.json", to: "id_tokens#jwks"
|
||||
match "user_info", to: "user_info#show", via: %i(get post)
|
||||
end
|
||||
end
|
||||
|
||||
get ".well-known/webfinger", to: "api/openid_connect/discovery#webfinger"
|
||||
get ".well-known/openid-configuration", to: "api/openid_connect/discovery#configuration"
|
||||
end
|
||||
|
|
|
|||
30
db/migrate/20150613202109_create_o_auth_applications.rb
Normal file
30
db/migrate/20150613202109_create_o_auth_applications.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/db/migrate/20110829023826_create_clients.rb
|
||||
|
||||
class CreateOAuthApplications < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :o_auth_applications do |t|
|
||||
t.belongs_to :user, index: true
|
||||
t.string :client_id, index: {unique: true, length: 191}
|
||||
t.string :client_secret
|
||||
t.string :client_name
|
||||
|
||||
t.text :redirect_uris
|
||||
t.string :response_types
|
||||
t.string :grant_types
|
||||
t.string :application_type, default: "web"
|
||||
t.string :contacts
|
||||
t.string :logo_uri
|
||||
t.string :client_uri
|
||||
t.string :policy_uri
|
||||
t.string :tos_uri
|
||||
t.string :sector_identifier_uri
|
||||
t.string :token_endpoint_auth_method
|
||||
t.text :jwks
|
||||
t.string :jwks_uri
|
||||
t.boolean :ppid, default: false
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_foreign_key :o_auth_applications, :users
|
||||
end
|
||||
end
|
||||
18
db/migrate/20150708153926_create_authorizations.rb
Normal file
18
db/migrate/20150708153926_create_authorizations.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
class CreateAuthorizations < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :authorizations do |t|
|
||||
t.belongs_to :user, index: true
|
||||
t.belongs_to :o_auth_application, index: true
|
||||
t.string :refresh_token
|
||||
t.string :code
|
||||
t.string :redirect_uri
|
||||
t.string :nonce
|
||||
t.string :scopes
|
||||
t.boolean :code_used, default: false
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_foreign_key :authorizations, :users
|
||||
add_foreign_key :authorizations, :o_auth_applications
|
||||
end
|
||||
end
|
||||
14
db/migrate/20150708153928_create_o_auth_access_tokens.rb
Normal file
14
db/migrate/20150708153928_create_o_auth_access_tokens.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/db/migrate/20110829023837_create_access_tokens.rb
|
||||
|
||||
class CreateOAuthAccessTokens < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :o_auth_access_tokens do |t|
|
||||
t.belongs_to :authorization, index: true
|
||||
t.string :token, index: {unique: true, length: 191}
|
||||
t.datetime :expires_at
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_foreign_key :o_auth_access_tokens, :authorizations
|
||||
end
|
||||
end
|
||||
14
db/migrate/20150714055110_create_id_tokens.rb
Normal file
14
db/migrate/20150714055110_create_id_tokens.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/db/migrate/20110829024010_create_id_tokens.rb
|
||||
|
||||
class CreateIdTokens < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :id_tokens do |t|
|
||||
t.belongs_to :authorization, index: true
|
||||
t.datetime :expires_at
|
||||
t.string :nonce
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_foreign_key :id_tokens, :authorizations
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/db/migrate/20110829024140_create_pairwise_pseudonymous_identifiers.rb
|
||||
|
||||
class CreatePairwisePseudonymousIdentifiers < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :ppid do |t|
|
||||
t.belongs_to :o_auth_application, index: true
|
||||
t.belongs_to :user, index: true
|
||||
|
||||
t.string :guid, :string, limit: 32
|
||||
t.string :identifier
|
||||
end
|
||||
add_foreign_key :ppid, :o_auth_applications
|
||||
add_foreign_key :ppid, :users
|
||||
end
|
||||
end
|
||||
83
db/schema.rb
83
db/schema.rb
|
|
@ -55,6 +55,22 @@ ActiveRecord::Schema.define(version: 20151003142048) do
|
|||
add_index "aspects", ["user_id", "contacts_visible"], name: "index_aspects_on_user_id_and_contacts_visible", using: :btree
|
||||
add_index "aspects", ["user_id"], name: "index_aspects_on_user_id", using: :btree
|
||||
|
||||
create_table "authorizations", force: :cascade do |t|
|
||||
t.integer "user_id", limit: 4
|
||||
t.integer "o_auth_application_id", limit: 4
|
||||
t.string "refresh_token", limit: 255
|
||||
t.string "code", limit: 255
|
||||
t.string "redirect_uri", limit: 255
|
||||
t.string "nonce", limit: 255
|
||||
t.string "scopes", limit: 255
|
||||
t.boolean "code_used", default: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "authorizations", ["o_auth_application_id"], name: "index_authorizations_on_o_auth_application_id", using: :btree
|
||||
add_index "authorizations", ["user_id"], name: "index_authorizations_on_user_id", using: :btree
|
||||
|
||||
create_table "blocks", force: :cascade do |t|
|
||||
t.integer "user_id", limit: 4
|
||||
t.integer "person_id", limit: 4
|
||||
|
|
@ -137,6 +153,16 @@ ActiveRecord::Schema.define(version: 20151003142048) do
|
|||
|
||||
add_index "conversations", ["author_id"], name: "conversations_author_id_fk", using: :btree
|
||||
|
||||
create_table "id_tokens", force: :cascade do |t|
|
||||
t.integer "authorization_id", limit: 4
|
||||
t.datetime "expires_at"
|
||||
t.string "nonce", limit: 255
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "id_tokens", ["authorization_id"], name: "index_id_tokens_on_authorization_id", using: :btree
|
||||
|
||||
create_table "invitation_codes", force: :cascade do |t|
|
||||
t.string "token", limit: 255
|
||||
t.integer "user_id", limit: 4
|
||||
|
|
@ -236,6 +262,43 @@ ActiveRecord::Schema.define(version: 20151003142048) do
|
|||
add_index "notifications", ["target_id"], name: "index_notifications_on_target_id", 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_access_tokens", force: :cascade do |t|
|
||||
t.integer "authorization_id", limit: 4
|
||||
t.string "token", limit: 255
|
||||
t.datetime "expires_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "o_auth_access_tokens", ["authorization_id"], name: "index_o_auth_access_tokens_on_authorization_id", using: :btree
|
||||
add_index "o_auth_access_tokens", ["token"], name: "index_o_auth_access_tokens_on_token", unique: true, length: {"token"=>191}, 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.string "client_name", limit: 255
|
||||
t.text "redirect_uris", limit: 65535
|
||||
t.string "response_types", limit: 255
|
||||
t.string "grant_types", limit: 255
|
||||
t.string "application_type", limit: 255, default: "web"
|
||||
t.string "contacts", limit: 255
|
||||
t.string "logo_uri", limit: 255
|
||||
t.string "client_uri", limit: 255
|
||||
t.string "policy_uri", limit: 255
|
||||
t.string "tos_uri", limit: 255
|
||||
t.string "sector_identifier_uri", limit: 255
|
||||
t.string "token_endpoint_auth_method", limit: 255
|
||||
t.text "jwks", limit: 65535
|
||||
t.string "jwks_uri", limit: 255
|
||||
t.boolean "ppid", default: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "o_auth_applications", ["client_id"], name: "index_o_auth_applications_on_client_id", unique: true, length: {"client_id"=>191}, using: :btree
|
||||
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
|
||||
|
|
@ -402,6 +465,17 @@ ActiveRecord::Schema.define(version: 20151003142048) do
|
|||
add_index "posts", ["tweet_id"], name: "index_posts_on_tweet_id", length: {"tweet_id"=>191}, using: :btree
|
||||
add_index "posts", ["type", "pending", "id"], name: "index_posts_on_type_and_pending_and_id", using: :btree
|
||||
|
||||
create_table "ppid", force: :cascade do |t|
|
||||
t.integer "o_auth_application_id", limit: 4
|
||||
t.integer "user_id", limit: 4
|
||||
t.string "guid", limit: 32
|
||||
t.string "string", limit: 32
|
||||
t.string "identifier", limit: 255
|
||||
end
|
||||
|
||||
add_index "ppid", ["o_auth_application_id"], name: "index_ppid_on_o_auth_application_id", using: :btree
|
||||
add_index "ppid", ["user_id"], name: "index_ppid_on_user_id", using: :btree
|
||||
|
||||
create_table "profiles", force: :cascade do |t|
|
||||
t.string "diaspora_handle", limit: 255
|
||||
t.string "first_name", limit: 127
|
||||
|
|
@ -419,7 +493,7 @@ ActiveRecord::Schema.define(version: 20151003142048) do
|
|||
t.string "location", limit: 255
|
||||
t.string "full_name", limit: 70
|
||||
t.boolean "nsfw", default: false
|
||||
t.boolean "public_details", default: false
|
||||
t.boolean "public_details", default: false
|
||||
end
|
||||
|
||||
add_index "profiles", ["full_name", "searchable"], name: "index_profiles_on_full_name_and_searchable", using: :btree
|
||||
|
|
@ -589,18 +663,25 @@ ActiveRecord::Schema.define(version: 20151003142048) do
|
|||
add_foreign_key "aspect_memberships", "aspects", name: "aspect_memberships_aspect_id_fk", on_delete: :cascade
|
||||
add_foreign_key "aspect_memberships", "contacts", name: "aspect_memberships_contact_id_fk", on_delete: :cascade
|
||||
add_foreign_key "aspect_visibilities", "aspects", name: "aspect_visibilities_aspect_id_fk", on_delete: :cascade
|
||||
add_foreign_key "authorizations", "o_auth_applications"
|
||||
add_foreign_key "authorizations", "users"
|
||||
add_foreign_key "comments", "people", column: "author_id", name: "comments_author_id_fk", on_delete: :cascade
|
||||
add_foreign_key "contacts", "people", name: "contacts_person_id_fk", on_delete: :cascade
|
||||
add_foreign_key "conversation_visibilities", "conversations", name: "conversation_visibilities_conversation_id_fk", on_delete: :cascade
|
||||
add_foreign_key "conversation_visibilities", "people", name: "conversation_visibilities_person_id_fk", on_delete: :cascade
|
||||
add_foreign_key "conversations", "people", column: "author_id", name: "conversations_author_id_fk", on_delete: :cascade
|
||||
add_foreign_key "id_tokens", "authorizations"
|
||||
add_foreign_key "invitations", "users", column: "recipient_id", name: "invitations_recipient_id_fk", on_delete: :cascade
|
||||
add_foreign_key "invitations", "users", column: "sender_id", name: "invitations_sender_id_fk", on_delete: :cascade
|
||||
add_foreign_key "likes", "people", column: "author_id", name: "likes_author_id_fk", on_delete: :cascade
|
||||
add_foreign_key "messages", "conversations", name: "messages_conversation_id_fk", on_delete: :cascade
|
||||
add_foreign_key "messages", "people", column: "author_id", name: "messages_author_id_fk", on_delete: :cascade
|
||||
add_foreign_key "notification_actors", "notifications", name: "notification_actors_notification_id_fk", on_delete: :cascade
|
||||
add_foreign_key "o_auth_access_tokens", "authorizations"
|
||||
add_foreign_key "o_auth_applications", "users"
|
||||
add_foreign_key "posts", "people", column: "author_id", name: "posts_author_id_fk", on_delete: :cascade
|
||||
add_foreign_key "ppid", "o_auth_applications"
|
||||
add_foreign_key "ppid", "users"
|
||||
add_foreign_key "profiles", "people", name: "profiles_person_id_fk", on_delete: :cascade
|
||||
add_foreign_key "services", "users", name: "services_user_id_fk", on_delete: :cascade
|
||||
add_foreign_key "share_visibilities", "contacts", name: "post_visibilities_contact_id_fk", on_delete: :cascade
|
||||
|
|
|
|||
26
features/desktop/oidc_auth_code_flow.feature
Normal file
26
features/desktop/oidc_auth_code_flow.feature
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
@javascript
|
||||
Feature: Access protected resources using auth code flow
|
||||
Background:
|
||||
Given a user with username "kent"
|
||||
|
||||
Scenario: Invalid client id to auth endpoint
|
||||
When I register a new client
|
||||
And I send a post request from that client to the code flow authorization endpoint using a invalid client id
|
||||
And I sign in as "kent@kent.kent"
|
||||
Then I should see a message containing "Invalid client id or redirect uri"
|
||||
|
||||
Scenario: Application is denied authorization
|
||||
When I register a new client
|
||||
And I send a post request from that client to the code flow authorization endpoint
|
||||
And I sign in as "kent@kent.kent"
|
||||
And I deny authorization to the client
|
||||
Then I should not see any tokens in the redirect url
|
||||
|
||||
Scenario: Application is authorized
|
||||
When I register a new client
|
||||
And I send a post request from that client to the code flow authorization endpoint
|
||||
And I sign in as "kent@kent.kent"
|
||||
And I give my consent and authorize the client
|
||||
And I parse the auth code and create a request to the token endpoint
|
||||
And I parse the tokens and use it obtain user info
|
||||
Then I should receive "kent"'s id, username, and email
|
||||
35
features/desktop/oidc_implicit_flow.feature
Normal file
35
features/desktop/oidc_implicit_flow.feature
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
@javascript
|
||||
Feature: Access protected resources using implicit flow
|
||||
Background:
|
||||
Given a user with username "kent"
|
||||
|
||||
Scenario: Invalid client id to auth endpoint
|
||||
When I register a new client
|
||||
And I send a post request from that client to the authorization endpoint using a invalid client id
|
||||
And I sign in as "kent@kent.kent"
|
||||
Then I should see a message containing "Invalid client id or redirect uri"
|
||||
|
||||
Scenario: Application is denied authorization
|
||||
When I register a new client
|
||||
And I send a post request from that client to the authorization endpoint
|
||||
And I sign in as "kent@kent.kent"
|
||||
And I deny authorization to the client
|
||||
Then I should not see any tokens in the redirect url
|
||||
|
||||
Scenario: Application is authorized
|
||||
When I register a new client
|
||||
And I send a post request from that client to the authorization endpoint
|
||||
And I sign in as "kent@kent.kent"
|
||||
And I give my consent and authorize the client
|
||||
And I parse the bearer tokens and use it to access user info
|
||||
Then I should receive "kent"'s id, username, and email
|
||||
|
||||
Scenario: Application is authorized and uses small value for the max_age parameter
|
||||
When I register a new client
|
||||
And I sign in as "kent@kent.kent"
|
||||
And I have signed in 5 minutes ago
|
||||
And I send a post request from that client to the authorization endpoint with max age
|
||||
And I sign in as "kent@kent.kent"
|
||||
And I give my consent and authorize the client
|
||||
And I parse the bearer tokens and use it to access user info
|
||||
Then I should receive "kent"'s id, username, and email
|
||||
23
features/desktop/user_applications.feature
Normal file
23
features/desktop/user_applications.feature
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
@javascript
|
||||
Feature: managing authorized applications
|
||||
Background:
|
||||
Given following users exist:
|
||||
| username | email |
|
||||
| Augier | augier@example.org |
|
||||
And a client with a provided picture exists for user "augier@example.org"
|
||||
And a client exists for user "augier@example.org"
|
||||
|
||||
Scenario: displaying authorizations
|
||||
When I sign in as "augier@example.org"
|
||||
And I go to the user applications page
|
||||
Then I should see 2 authorized applications
|
||||
And I should see 1 authorized applications with no provided image
|
||||
And I should see 1 authorized applications with an image
|
||||
|
||||
Scenario: revoke an authorization
|
||||
When I sign in as "augier@example.org"
|
||||
And I go to the user applications page
|
||||
And I revoke the first authorization
|
||||
Then I should see 1 authorized applications
|
||||
And I revoke the first authorization
|
||||
Then I should see 0 authorized applications
|
||||
24
features/mobile/user_applications.feature
Normal file
24
features/mobile/user_applications.feature
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
@mobile
|
||||
@javascript
|
||||
Feature: managing authorized applications
|
||||
Background:
|
||||
Given following users exist:
|
||||
| username | email |
|
||||
| Augier | augier@example.org |
|
||||
And a client with a provided picture exists for user "augier@example.org"
|
||||
And a client exists for user "augier@example.org"
|
||||
|
||||
Scenario: displaying authorizations
|
||||
When I sign in as "augier@example.org"
|
||||
And I go to the user applications page
|
||||
Then I should see 2 authorized applications
|
||||
And I should see 1 authorized applications with no provided image
|
||||
And I should see 1 authorized applications with an image
|
||||
|
||||
Scenario: revoke an authorization
|
||||
When I sign in as "augier@example.org"
|
||||
And I go to the user applications page
|
||||
And I revoke the first authorization
|
||||
Then I should see 1 authorized applications
|
||||
And I revoke the first authorization
|
||||
Then I should see 0 authorized applications
|
||||
37
features/step_definitions/auth_code_steps.rb
Normal file
37
features/step_definitions/auth_code_steps.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
O_AUTH_QUERY_PARAMS_WITH_CODE = {
|
||||
redirect_uri: "http://localhost:3000",
|
||||
response_type: "code",
|
||||
scope: "openid profile read",
|
||||
nonce: "hello",
|
||||
state: "hi"
|
||||
}
|
||||
|
||||
Given /^I send a post request from that client to the code flow authorization endpoint$/ do
|
||||
client_json = JSON.parse(last_response.body)
|
||||
@client_id = client_json["client_id"]
|
||||
@client_secret = client_json["client_secret"]
|
||||
params = O_AUTH_QUERY_PARAMS_WITH_CODE.merge(client_id: @client_id)
|
||||
visit new_api_openid_connect_authorization_path(params)
|
||||
end
|
||||
|
||||
Given /^I send a post request from that client to the code flow authorization endpoint using a invalid client id/ do
|
||||
params = O_AUTH_QUERY_PARAMS_WITH_CODE.merge(client_id: "randomid")
|
||||
visit new_api_openid_connect_authorization_path(params)
|
||||
end
|
||||
|
||||
When /^I parse the auth code and create a request to the token endpoint$/ do
|
||||
code = current_url[/(?<=code=)[^&]+/]
|
||||
post api_openid_connect_access_tokens_path, code: code,
|
||||
redirect_uri: "http://localhost:3000", grant_type: "authorization_code",
|
||||
client_id: @client_id, client_secret: @client_secret
|
||||
end
|
||||
|
||||
When /^I parse the tokens and use it obtain user info$/ do
|
||||
client_json = JSON.parse(last_response.body)
|
||||
access_token = client_json["access_token"]
|
||||
encoded_id_token = client_json["id_token"]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
expect(decoded_token.sub).to eq(@me.diaspora_handle)
|
||||
get api_openid_connect_user_info_path, access_token: access_token
|
||||
end
|
||||
61
features/step_definitions/implicit_flow_steps.rb
Normal file
61
features/step_definitions/implicit_flow_steps.rb
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
O_AUTH_QUERY_PARAMS = {
|
||||
redirect_uri: "http://localhost:3000",
|
||||
response_type: "id_token token",
|
||||
scope: "openid profile read",
|
||||
nonce: "hello",
|
||||
state: "hi",
|
||||
prompt: "login"
|
||||
}
|
||||
|
||||
O_AUTH_QUERY_PARAMS_WITH_MAX_AGE = {
|
||||
redirect_uri: "http://localhost:3000",
|
||||
response_type: "id_token token",
|
||||
scope: "openid profile read",
|
||||
nonce: "hello",
|
||||
state: "hi",
|
||||
prompt: "login",
|
||||
max_age: 30
|
||||
}
|
||||
|
||||
Given /^I send a post request from that client to the authorization endpoint$/ do
|
||||
client_json = JSON.parse(last_response.body)
|
||||
visit new_api_openid_connect_authorization_path(O_AUTH_QUERY_PARAMS.merge(client_id: client_json["client_id"]))
|
||||
end
|
||||
|
||||
Given /^I have signed in (\d+) minutes ago$/ do |minutes|
|
||||
@me.update_attribute(:current_sign_in_at, Time.zone.now - minutes.to_i.minute)
|
||||
end
|
||||
|
||||
Given /^I send a post request from that client to the authorization endpoint with max age$/ do
|
||||
client_json = JSON.parse(last_response.body)
|
||||
visit new_api_openid_connect_authorization_path(
|
||||
O_AUTH_QUERY_PARAMS_WITH_MAX_AGE.merge(client_id: client_json["client_id"]))
|
||||
end
|
||||
|
||||
Given /^I send a post request from that client to the authorization endpoint using a invalid client id$/ do
|
||||
visit new_api_openid_connect_authorization_path(O_AUTH_QUERY_PARAMS.merge(client_id: "randomid"))
|
||||
end
|
||||
|
||||
When /^I give my consent and authorize the client$/ do
|
||||
click_button "Approve"
|
||||
end
|
||||
|
||||
When /^I deny authorization to the client$/ do
|
||||
click_button "Deny"
|
||||
end
|
||||
|
||||
Then /^I should not see any tokens in the redirect url$/ do
|
||||
access_token = current_url[/(?<=access_token=)[^&]+/]
|
||||
id_token = current_url[/(?<=access_token=)[^&]+/]
|
||||
expect(access_token).to eq(nil)
|
||||
expect(id_token).to eq(nil)
|
||||
end
|
||||
|
||||
When /^I parse the bearer tokens and use it to access user info$/ do
|
||||
access_token = current_url[/(?<=access_token=)[^&]+/]
|
||||
get api_openid_connect_user_info_path, access_token: access_token
|
||||
end
|
||||
|
||||
Then /^I should see an "([^\"]*)" error$/ do |error_message|
|
||||
expect(page).to have_content(error_message)
|
||||
end
|
||||
39
features/step_definitions/oidc_common_steps.rb
Normal file
39
features/step_definitions/oidc_common_steps.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
Given /^a client with a provided picture exists for user "([^\"]*)"$/ do |email|
|
||||
app = FactoryGirl.create(:o_auth_application_with_image)
|
||||
user = User.find_by(email: email)
|
||||
FactoryGirl.create(:auth_with_read, user: user, o_auth_application: app)
|
||||
end
|
||||
|
||||
Given /^a client exists for user "([^\"]*)"$/ do |email|
|
||||
user = User.find_by(email: email)
|
||||
FactoryGirl.create(:auth_with_read, user: user)
|
||||
end
|
||||
|
||||
When /^I register a new client$/ do
|
||||
post api_openid_connect_clients_path, redirect_uris: ["http://localhost:3000"], client_name: "diaspora client"
|
||||
end
|
||||
|
||||
When /^I use received valid bearer tokens to access user info$/ do
|
||||
access_token_json = JSON.parse(last_response.body)
|
||||
get api_openid_connect_user_info_path, access_token: access_token_json["access_token"]
|
||||
end
|
||||
|
||||
When /^I use invalid bearer tokens to access user info$/ do
|
||||
get api_openid_connect_user_info_path, access_token: SecureRandom.hex(32)
|
||||
end
|
||||
|
||||
Then /^I should receive "([^\"]*)"'s id, username, and email$/ do |username|
|
||||
user_info_json = JSON.parse(last_response.body)
|
||||
user = User.find_by_username(username)
|
||||
user_profile_url = File.join(AppConfig.environment.url, "people", user.guid).to_s
|
||||
expect(user_info_json["profile"]).to have_content(user_profile_url)
|
||||
end
|
||||
|
||||
Then /^I should receive an "([^\"]*)" error$/ do |error_message|
|
||||
user_info_json = JSON.parse(last_response.body)
|
||||
expect(user_info_json["error"]).to have_content(error_message)
|
||||
end
|
||||
|
||||
Then(/^I should see a message containing "(.*?)"$/) do |message|
|
||||
expect(find("#openid_connect_error_description").text).to include(message)
|
||||
end
|
||||
16
features/step_definitions/user_applications_steps.rb
Normal file
16
features/step_definitions/user_applications_steps.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
Then /^I should see (\d+) authorized applications$/ do |num|
|
||||
expect(page).to have_selector(".applications-page", count: 1)
|
||||
expect(page).to have_selector(".authorized-application", count: num.to_i)
|
||||
end
|
||||
|
||||
Then /^I should see (\d+) authorized applications with no provided image$/ do |num|
|
||||
expect(page).to have_selector(".application-img > .entypo-browser", count: num.to_i)
|
||||
end
|
||||
|
||||
Then /^I should see (\d+) authorized applications with an image$/ do |num|
|
||||
expect(page).to have_selector(".application-img > .img-responsive", count: num.to_i)
|
||||
end
|
||||
|
||||
When /^I revoke the first authorization$/ do
|
||||
find(".app-revoke", match: :first).click
|
||||
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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module NavigationHelpers
|
|||
stream_path
|
||||
when /^the mobile path$/
|
||||
force_mobile_path
|
||||
when /^the user applications page$/
|
||||
api_openid_connect_user_applications_path
|
||||
when /^the tag page for "([^\"]*)"$/
|
||||
tag_path(Regexp.last_match(1))
|
||||
when /^its ([\w ]+) page$/
|
||||
|
|
|
|||
|
|
@ -46,15 +46,17 @@ class AccountDeleter
|
|||
|
||||
#user deletions
|
||||
def normal_ar_user_associates_to_delete
|
||||
[:tag_followings, :invitations_to_me, :services, :aspects, :user_preferences, :notifications, :blocks]
|
||||
%i(tag_followings invitations_to_me services aspects user_preferences
|
||||
notifications blocks authorizations o_auth_applications pairwise_pseudonymous_identifiers)
|
||||
end
|
||||
|
||||
def special_ar_user_associations
|
||||
[:invitations_from_me, :person, :profile, :contacts, :auto_follow_back_aspect]
|
||||
%i(invitations_from_me person profile contacts auto_follow_back_aspect)
|
||||
end
|
||||
|
||||
def ignored_ar_user_associations
|
||||
[:followed_tags, :invited_by, :contact_people, :aspect_memberships, :ignored_people, :conversation_visibilities, :conversations, :reports]
|
||||
%i(followed_tags invited_by contact_people aspect_memberships
|
||||
ignored_people conversation_visibilities conversations reports)
|
||||
end
|
||||
|
||||
def delete_standard_user_associations
|
||||
|
|
|
|||
62
lib/api/openid_connect/authorization_point/endpoint.rb
Normal file
62
lib/api/openid_connect/authorization_point/endpoint.rb
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
module AuthorizationPoint
|
||||
class Endpoint
|
||||
attr_accessor :app, :user, :o_auth_application, :redirect_uri, :response_type,
|
||||
:scopes, :request_uri, :request_object, :nonce
|
||||
delegate :call, to: :app
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
@app = Rack::OAuth2::Server::Authorize.new do |req, res|
|
||||
build_from_request_object(req)
|
||||
build_attributes(req, res)
|
||||
if OAuthApplication.available_response_types.include? Array(req.response_type).join(" ")
|
||||
handle_response_type(req, res)
|
||||
else
|
||||
req.unsupported_response_type!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_attributes(req, res)
|
||||
build_client(req)
|
||||
build_redirect_uri(req, res)
|
||||
verify_nonce(req, res)
|
||||
build_scopes(req)
|
||||
end
|
||||
|
||||
def handle_response_type(_req, _res)
|
||||
raise NotImplementedError # Implemented by subclass
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_client(req)
|
||||
@o_auth_application = OAuthApplication.find_by_client_id(req.client_id) || req.bad_request!
|
||||
end
|
||||
|
||||
def build_redirect_uri(req, res)
|
||||
res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@o_auth_application.redirect_uris)
|
||||
end
|
||||
|
||||
def verify_nonce(req, res)
|
||||
req.invalid_request! "nonce required" if res.protocol_params_location == :fragment && req.nonce.blank?
|
||||
end
|
||||
|
||||
def build_scopes(req)
|
||||
replace_profile_scope_with_specific_claims(req)
|
||||
@scopes = req.scope.map {|scope|
|
||||
scope.tap do |scope_name|
|
||||
req.invalid_scope! "Unknown scope: #{scope_name}" unless auth_scopes.include? scope_name
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def auth_scopes
|
||||
Api::OpenidConnect::Authorization::SCOPES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
module AuthorizationPoint
|
||||
class EndpointConfirmationPoint < Endpoint
|
||||
def initialize(current_user, approved=false)
|
||||
super(current_user)
|
||||
@approved = approved
|
||||
end
|
||||
|
||||
def handle_response_type(req, res)
|
||||
handle_approval(@approved, req, res)
|
||||
end
|
||||
|
||||
def handle_approval(approved, req, res)
|
||||
if approved
|
||||
approved!(req, res)
|
||||
else
|
||||
req.access_denied!
|
||||
end
|
||||
end
|
||||
|
||||
def replace_profile_scope_with_specific_claims(_req)
|
||||
# Empty
|
||||
end
|
||||
|
||||
def build_from_request_object(_req)
|
||||
# Empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def approved!(req, res)
|
||||
auth = find_or_build_auth(req)
|
||||
handle_approved_response_type(auth, req, res)
|
||||
res.approve!
|
||||
end
|
||||
|
||||
def find_or_build_auth(req)
|
||||
OpenidConnect::Authorization.find_or_create_by!(
|
||||
o_auth_application: @o_auth_application, user: @user, redirect_uri: @redirect_uri).tap do |auth|
|
||||
auth.nonce = req.nonce
|
||||
auth.scopes = @scopes
|
||||
auth.save
|
||||
end
|
||||
end
|
||||
|
||||
def handle_approved_response_type(auth, req, res)
|
||||
response_types = Array(req.response_type)
|
||||
handle_approved_auth_code(auth, res, response_types)
|
||||
handle_approved_access_token(auth, res, response_types)
|
||||
handle_approved_id_token(auth, res, response_types)
|
||||
end
|
||||
|
||||
def handle_approved_auth_code(auth, res, response_types)
|
||||
return unless response_types.include?(:code)
|
||||
res.code = auth.create_code
|
||||
end
|
||||
|
||||
def handle_approved_access_token(auth, res, response_types)
|
||||
return unless response_types.include?(:token)
|
||||
res.access_token = auth.create_access_token
|
||||
end
|
||||
|
||||
def handle_approved_id_token(auth, res, response_types)
|
||||
return unless response_types.include?(:id_token)
|
||||
id_token = auth.create_id_token
|
||||
res.id_token = id_token.to_jwt(code: res.try(:code), access_token: res.try(:access_token))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
module AuthorizationPoint
|
||||
class EndpointStartPoint < Endpoint
|
||||
def build_from_request_object(req)
|
||||
request_object = build_request_object(req)
|
||||
return unless request_object
|
||||
claims = request_object.raw_attributes.with_indifferent_access[:claims].try(:[], :userinfo).try(:keys)
|
||||
return unless claims
|
||||
req.update_param("scope", req.scope + claims)
|
||||
end
|
||||
|
||||
def handle_response_type(req, _res)
|
||||
@response_type = req.response_type
|
||||
end
|
||||
|
||||
def replace_profile_scope_with_specific_claims(req)
|
||||
profile_claims = %w(sub aud name nickname profile picture)
|
||||
scopes_as_claims = req.scope.flat_map {|scope| scope == "profile" ? profile_claims : [scope] }.uniq
|
||||
req.update_param("scope", scopes_as_claims)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_request_object(req)
|
||||
if req.request_uri.present?
|
||||
OpenIDConnect::RequestObject.fetch req.request_uri
|
||||
elsif req.request.present?
|
||||
OpenIDConnect::RequestObject.decode req.request
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
16
lib/api/openid_connect/error.rb
Normal file
16
lib/api/openid_connect/error.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
module Error
|
||||
class InvalidRedirectUri < ::ArgumentError
|
||||
def initialize
|
||||
super "Redirect uri contains fragment"
|
||||
end
|
||||
end
|
||||
class InvalidSectorIdentifierUri < ::ArgumentError
|
||||
def initialize
|
||||
super "Invalid sector identifier uri"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
16
lib/api/openid_connect/id_token_config.rb
Normal file
16
lib/api/openid_connect/id_token_config.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
class IdTokenConfig
|
||||
key_file_path = File.join(Rails.root, "config", "oidc_key.pem")
|
||||
if File.exist?(key_file_path)
|
||||
private_key = OpenSSL::PKey::RSA.new(File.read(key_file_path))
|
||||
else
|
||||
private_key = OpenSSL::PKey::RSA.new(2048)
|
||||
File.write key_file_path, private_key.to_pem
|
||||
File.chmod(0600, key_file_path)
|
||||
end
|
||||
PRIVATE_KEY = private_key
|
||||
PUBLIC_KEY = private_key.public_key
|
||||
end
|
||||
end
|
||||
end
|
||||
38
lib/api/openid_connect/protected_resource_endpoint.rb
Normal file
38
lib/api/openid_connect/protected_resource_endpoint.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Copyright (c) 2011 nov matake
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# See https://github.com/nov/openid_connect_sample/blob/master/lib/authentication.rb#L56
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
module ProtectedResourceEndpoint
|
||||
attr_reader :current_token
|
||||
|
||||
def require_access_token(required_scopes)
|
||||
@current_token = request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
|
||||
raise Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new("Unauthorized user") unless
|
||||
@current_token && @current_token.authorization
|
||||
raise Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(:insufficient_scope) unless
|
||||
@current_token.authorization.try(:accessible?, required_scopes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
lib/api/openid_connect/subject_identifier_creator.rb
Normal file
17
lib/api/openid_connect/subject_identifier_creator.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
module Api
|
||||
module OpenidConnect
|
||||
module SubjectIdentifierCreator
|
||||
def self.create(auth)
|
||||
if auth.o_auth_application.ppid?
|
||||
identifier = auth.o_auth_application.sector_identifier_uri ||
|
||||
URI.parse(auth.o_auth_application.redirect_uris[0]).host
|
||||
pairwise_pseudonymous_identifier =
|
||||
auth.user.pairwise_pseudonymous_identifiers.find_or_create_by(identifier: identifier)
|
||||
pairwise_pseudonymous_identifier.guid
|
||||
else
|
||||
auth.user.diaspora_handle
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
57
lib/api/openid_connect/token_endpoint.rb
Normal file
57
lib/api/openid_connect/token_endpoint.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Inspired by https://github.com/nov/openid_connect_sample/blob/master/lib/token_endpoint.rb
|
||||
|
||||
module Api
|
||||
module OpenidConnect
|
||||
class TokenEndpoint
|
||||
attr_accessor :app
|
||||
delegate :call, to: :app
|
||||
|
||||
def initialize
|
||||
@app = Rack::OAuth2::Server::Token.new do |req, res|
|
||||
o_auth_app = retrieve_client(req)
|
||||
if app_valid?(o_auth_app, req)
|
||||
handle_flows(req, res)
|
||||
else
|
||||
req.invalid_client!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_flows(req, res)
|
||||
case req.grant_type
|
||||
when :refresh_token
|
||||
handle_refresh_flow(req, res)
|
||||
when :authorization_code
|
||||
auth = Api::OpenidConnect::Authorization.with_redirect_uri(req.redirect_uri).use_code(req.code)
|
||||
req.invalid_grant! if auth.blank?
|
||||
res.access_token = auth.create_access_token
|
||||
if auth.accessible? "openid"
|
||||
id_token = auth.create_id_token
|
||||
res.id_token = id_token.to_jwt(access_token: res.access_token)
|
||||
end
|
||||
else
|
||||
req.unsupported_grant_type!
|
||||
end
|
||||
end
|
||||
|
||||
def handle_refresh_flow(req, res)
|
||||
# Handle as if scope request was omitted even if provided.
|
||||
# See https://tools.ietf.org/html/rfc6749#section-6 for handling
|
||||
auth = Api::OpenidConnect::Authorization.find_by_refresh_token req.client_id, req.refresh_token
|
||||
if auth
|
||||
res.access_token = auth.create_access_token
|
||||
else
|
||||
req.invalid_grant!
|
||||
end
|
||||
end
|
||||
|
||||
def retrieve_client(req)
|
||||
Api::OpenidConnect::OAuthApplication.find_by client_id: req.client_id
|
||||
end
|
||||
|
||||
def app_valid?(o_auth_app, req)
|
||||
o_auth_app.client_secret == req.client_secret
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::OpenidConnect::AuthorizationsController, type: :controller do
|
||||
let!(:client) { FactoryGirl.create(:o_auth_application) }
|
||||
let!(:client_with_xss) { FactoryGirl.create(:o_auth_application_with_xss) }
|
||||
let!(:client_with_multiple_redirects) { FactoryGirl.create(:o_auth_application_with_multiple_redirects) }
|
||||
let!(:auth_with_read) { FactoryGirl.create(:auth_with_read) }
|
||||
|
||||
before do
|
||||
sign_in :user, alice
|
||||
end
|
||||
|
||||
describe "#new" do
|
||||
context "when not yet authorized" do
|
||||
context "when valid parameters are passed" do
|
||||
render_views
|
||||
context "as GET request" do
|
||||
it "should return a form page" do
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to match("Diaspora Test Client")
|
||||
end
|
||||
end
|
||||
|
||||
context "using claims" do
|
||||
it "should return a form page" do
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", claims: "{\"userinfo\": {\"name\": {\"essential\": true}}}",
|
||||
nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to match("Diaspora Test Client")
|
||||
end
|
||||
end
|
||||
|
||||
context "as a request object" do
|
||||
it "should return a form page" do
|
||||
header = JWT.encoded_header("none")
|
||||
payload_hash = {client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", nonce: "hello", state: "hello",
|
||||
claims: {userinfo: {name: {essential: true}}}}
|
||||
payload = JWT.encoded_payload(JSON.parse(payload_hash.to_json))
|
||||
request_object = header + "." + payload + "."
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", nonce: "hello", state: "hello", request: request_object
|
||||
expect(response.body).to match("Diaspora Test Client")
|
||||
end
|
||||
end
|
||||
|
||||
context "as a request object with no claims" do
|
||||
it "should return a form page" do
|
||||
header = JWT.encoded_header("none")
|
||||
payload_hash = {client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", nonce: "hello", state: "hello"}
|
||||
payload = JWT.encoded_payload(JSON.parse(payload_hash.to_json))
|
||||
request_object = header + "." + payload + "."
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", nonce: "hello", state: "hello", request: request_object
|
||||
expect(response.body).to match("Diaspora Test Client")
|
||||
end
|
||||
end
|
||||
|
||||
context "as POST request" do
|
||||
it "should return a form page" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to match("Diaspora Test Client")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when client id is missing" do
|
||||
it "should return an bad request error" do
|
||||
post :new, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to include("The request was malformed")
|
||||
end
|
||||
end
|
||||
|
||||
context "when redirect uri is missing" do
|
||||
context "when only one redirect URL is pre-registered" do
|
||||
it "should return a form page" do
|
||||
# Note this intentionally behavior diverts from OIDC spec http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
# When client has only one redirect uri registered, only that redirect uri can be used. Hence,
|
||||
# we should implicitly assume the client wants to use that registered URI.
|
||||
# See https://github.com/nov/rack-oauth2/blob/master/lib/rack/oauth2/server/authorize.rb#L63
|
||||
post :new, client_id: client.client_id, response_type: "id_token",
|
||||
scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to match("Diaspora Test Client")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when multiple redirect URLs are pre-registered" do
|
||||
it "should return an invalid request error" do
|
||||
post :new, client_id: client_with_multiple_redirects.client_id, response_type: "id_token",
|
||||
scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to include("The request was malformed")
|
||||
end
|
||||
end
|
||||
|
||||
context "when redirect URI does not match pre-registered URIs" do
|
||||
it "should return an invalid request error", focus: true do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:2000/",
|
||||
response_type: "id_token", scope: "openid", nonce: SecureRandom.hex(16)
|
||||
expect(response.body).to include("Invalid client id or redirect uri")
|
||||
end
|
||||
end
|
||||
|
||||
context "when an unsupported scope is passed in" do
|
||||
it "should return an invalid scope error" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "random", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to match("error=invalid_scope")
|
||||
end
|
||||
end
|
||||
|
||||
context "when nonce is missing" do
|
||||
it "should return an invalid request error" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", state: SecureRandom.hex(16)
|
||||
expect(response.location).to match("error=invalid_request")
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt is none" do
|
||||
it "should return an interaction required error" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
|
||||
expect(response.body).to include("User must already be authorized when `prompt` is `none`")
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt is none and user not signed in" do
|
||||
before do
|
||||
sign_out :user
|
||||
end
|
||||
|
||||
it "should return an interaction required error" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
|
||||
expect(response.body).to include("User must already be logged in when `prompt` is `none`")
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt is none and consent" do
|
||||
it "should return an interaction required error" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none consent"
|
||||
expect(response.location).to match("error=invalid_request")
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt is select_account" do
|
||||
it "should return an account_selection_required error" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "select_account"
|
||||
expect(response.location).to match("error=account_selection_required")
|
||||
expect(response.location).to match("state=1234")
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt is none and client ID is invalid" do
|
||||
it "should return an account_selection_required error" do
|
||||
post :new, client_id: "random", redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
|
||||
expect(response.body).to include("Invalid client id or redirect uri")
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt is none and redirect URI does not match pre-registered URIs" do
|
||||
it "should return an account_selection_required error" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://randomuri:3000/",
|
||||
response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
|
||||
expect(response.body).to include("Invalid client id or redirect uri")
|
||||
end
|
||||
end
|
||||
|
||||
context "when XSS script is passed as name" do
|
||||
it "should escape html" do
|
||||
post :new, client_id: client_with_xss.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
|
||||
expect(response.body).to_not include("<script>alert(0);</script>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when already authorized" do
|
||||
let!(:auth) {
|
||||
Api::OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: alice,
|
||||
redirect_uri: "http://localhost:3000/", scopes: ["openid"])
|
||||
}
|
||||
|
||||
context "when valid parameters are passed" do
|
||||
before do
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3
|
||||
end
|
||||
|
||||
it "should return the id token in a fragment" do
|
||||
expect(response.location).to have_content("id_token=")
|
||||
encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
expect(decoded_token.nonce).to eq("4130930983")
|
||||
expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
|
||||
end
|
||||
|
||||
it "should return the passed in state" do
|
||||
expect(response.location).to have_content("state=4130930983")
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt is none" do
|
||||
it "should return the id token in a fragment" do
|
||||
post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3,
|
||||
display: "page", prompt: "none"
|
||||
expect(response.location).to have_content("id_token=")
|
||||
encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
expect(decoded_token.nonce).to eq("4130930983")
|
||||
expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
|
||||
end
|
||||
end
|
||||
|
||||
context "when prompt contains consent" do
|
||||
it "should return a consent form page" do
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token", scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3,
|
||||
display: "page", prompt: "consent"
|
||||
expect(response.body).to match("Diaspora Test Client")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
context "when id_token token" do
|
||||
before do
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token token",
|
||||
scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
|
||||
end
|
||||
|
||||
context "when authorization is approved" do
|
||||
before do
|
||||
post :create, approve: "true"
|
||||
end
|
||||
|
||||
it "should return the id token in a fragment" do
|
||||
encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
expect(decoded_token.nonce).to eq("4180930983")
|
||||
expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
|
||||
end
|
||||
|
||||
it "should return a valid access token in a fragment" do
|
||||
encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
access_token = response.location[/(?<=access_token=)[^&]+/]
|
||||
access_token_check_num = UrlSafeBase64.encode64(OpenSSL::Digest::SHA256.digest(access_token)[0, 128 / 8])
|
||||
expect(decoded_token.at_hash).to eq(access_token_check_num)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when id_token" do
|
||||
before do
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||
scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
|
||||
end
|
||||
|
||||
context "when authorization is approved" do
|
||||
before do
|
||||
post :create, approve: "true"
|
||||
end
|
||||
|
||||
it "should return the id token in a fragment" do
|
||||
expect(response.location).to have_content("id_token=")
|
||||
encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
expect(decoded_token.nonce).to eq("4180930983")
|
||||
expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
|
||||
end
|
||||
|
||||
it "should return the passed in state" do
|
||||
expect(response.location).to have_content("state=4180930983")
|
||||
end
|
||||
end
|
||||
|
||||
context "when authorization is denied" do
|
||||
before do
|
||||
post :create, approve: "false"
|
||||
end
|
||||
|
||||
it "should return an error in the fragment" do
|
||||
expect(response.location).to have_content("error=")
|
||||
end
|
||||
|
||||
it "should NOT contain a id token in the fragment" do
|
||||
expect(response.location).to_not have_content("id_token=")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when code" do
|
||||
before do
|
||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "code",
|
||||
scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
|
||||
end
|
||||
|
||||
context "when authorization is approved" do
|
||||
before do
|
||||
post :create, approve: "true"
|
||||
end
|
||||
|
||||
it "should return the code" do
|
||||
expect(response.location).to have_content("code")
|
||||
end
|
||||
|
||||
it "should return the passed in state" do
|
||||
expect(response.location).to have_content("state=4180930983")
|
||||
end
|
||||
end
|
||||
|
||||
context "when authorization is denied" do
|
||||
before do
|
||||
post :create, approve: "false"
|
||||
end
|
||||
|
||||
it "should return an error" do
|
||||
expect(response.location).to have_content("error")
|
||||
end
|
||||
|
||||
it "should NOT contain code" do
|
||||
expect(response.location).to_not have_content("code")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#destroy" do
|
||||
context "with existent authorization" do
|
||||
before do
|
||||
delete :destroy, id: auth_with_read.id
|
||||
end
|
||||
|
||||
it "removes the authorization" do
|
||||
expect(Api::OpenidConnect::Authorization.find_by(id: auth_with_read.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "with non-existent authorization" do
|
||||
it "raises an error" do
|
||||
delete :destroy, id: 123_456_789
|
||||
expect(response).to redirect_to(api_openid_connect_user_applications_url)
|
||||
expect(flash[:error]).to eq("The attempt to revoke the authorization with ID 123456789 failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
141
spec/controllers/api/openid_connect/clients_controller_spec.rb
Normal file
141
spec/controllers/api/openid_connect/clients_controller_spec.rb
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::OpenidConnect::ClientsController, type: :controller do
|
||||
describe "#create" do
|
||||
context "when valid parameters are passed" do
|
||||
it "should return a client id" do
|
||||
stub_request(:get, "http://example.com/uris")
|
||||
.with(headers: {
|
||||
"Accept" => "*/*",
|
||||
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
|
||||
"User-Agent" => "Faraday v0.9.2"
|
||||
})
|
||||
.to_return(status: 200, body: "[\"http://localhost\"]", headers: {})
|
||||
post :create, redirect_uris: ["http://localhost"], client_name: "diaspora client",
|
||||
response_types: [], grant_types: [], application_type: "web", contacts: [],
|
||||
logo_uri: "http://example.com/logo.png", client_uri: "http://example.com/client",
|
||||
policy_uri: "http://example.com/policy", tos_uri: "http://example.com/tos",
|
||||
sector_identifier_uri: "http://example.com/uris", subject_type: "pairwise"
|
||||
client_json = JSON.parse(response.body)
|
||||
expect(client_json["client_id"].length).to eq(32)
|
||||
expect(client_json["ppid"]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when valid parameters with jwks is passed" do
|
||||
it "should return a client id" do
|
||||
stub_request(:get, "http://example.com/uris")
|
||||
.with(headers: {
|
||||
"Accept" => "*/*",
|
||||
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
|
||||
"User-Agent" => "Faraday v0.9.2"})
|
||||
.to_return(status: 200, body: "[\"http://localhost\"]", headers: {})
|
||||
post :create, redirect_uris: ["http://localhost"], client_name: "diaspora client",
|
||||
response_types: [], grant_types: [], application_type: "web", contacts: [],
|
||||
logo_uri: "http://example.com/logo.png", client_uri: "http://example.com/client",
|
||||
policy_uri: "http://example.com/policy", tos_uri: "http://example.com/tos",
|
||||
sector_identifier_uri: "http://example.com/uris", subject_type: "pairwise",
|
||||
token_endpoint_auth_method: "private_key_jwt",
|
||||
jwks: {
|
||||
keys:
|
||||
[
|
||||
{
|
||||
use: "enc",
|
||||
e: "AQAB",
|
||||
d: "-lTBWkI-----lvCO6tuiDsR4qgJnUwnndQFwEI_4mLmD3iNWXrc8N--5Cjq55eLtuJjtvuQ",
|
||||
n: "--zYRQNDvIVsBDLQQIgrbctuGqj6lrXb31Jj3JIEYqH_4h5X9d0Q",
|
||||
q: "1q-r----pFtyTz_JksYYaotc_Z3Zy-Szw6a39IDbuYGy1qL-15oQuc",
|
||||
p: "-BfRjdgYouy4c6xAnGDgSMTip1YnPRyvbMaoYT9E_tEcBW5wOeoc",
|
||||
kid: "a0",
|
||||
kty: "RSA"
|
||||
},
|
||||
{
|
||||
use: "sig",
|
||||
e: "AQAB",
|
||||
d: "--x-gW---LRPowKrdvTuTo2p--HMI0pIEeFs7H_u5OW3jihjvoFClGPynHQhgWmQzlQRvWRXh6FhDVqFeGQ",
|
||||
n: "---TyeadDqQPWgbqX69UzcGq5irhzN8cpZ_JaTk3Y_uV6owanTZLVvCgdjaAnMYeZhb0KFw",
|
||||
q: "5E5XKK5njT--Hx3nF5sne5fleVfU-sZy6Za4B2U75PcE62oZgCPauOTAEm9Xuvrt5aMMovyzR8ecJZhm9bw7naU",
|
||||
p: "-BUGA-",
|
||||
kid: "a1",
|
||||
kty: "RSA"},
|
||||
{
|
||||
use: "sig",
|
||||
crv: "P-256",
|
||||
kty: "EC",
|
||||
y: "Yg4IRzHBMIsuQK2Oz0Uukp1aNDnpdoyk6QBMtmfGHQQ",
|
||||
x: "L0WUeVlc9r6YJd6ie9duvOU1RHwxSkJKA37IK9B4Bpc",
|
||||
kid: "a2"
|
||||
},
|
||||
{
|
||||
use: "enc",
|
||||
crv: "P-256",
|
||||
kty: "EC",
|
||||
y: "E6E6g5_ziIZvfdAoACctnwOhuQYMvQzA259aftPn59M",
|
||||
x: "Yu8_BQE2L0f1MqnK0GumZOaj_77Tx70-LoudyRUnLM4",
|
||||
kid: "a3"
|
||||
}
|
||||
]
|
||||
}
|
||||
client_json = JSON.parse(response.body)
|
||||
expect(client_json["client_id"].length).to eq(32)
|
||||
expect(client_json["ppid"]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when valid parameters with jwks_uri is passed" do
|
||||
it "should return a client id" do
|
||||
stub_request(:get, "http://example.com/uris")
|
||||
.with(headers: {"Accept" => "*/*",
|
||||
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
|
||||
"User-Agent" => "Faraday v0.9.2"})
|
||||
.to_return(status: 200, body: "[\"http://localhost\"]", headers: {})
|
||||
stub_request(:get, "https://kentshikama.com/api/openid_connect/jwks.json")
|
||||
.with(headers: {"Accept" => "*/*",
|
||||
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
|
||||
"User-Agent" => "Faraday v0.9.2"})
|
||||
.to_return(status: 200,
|
||||
body: "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"qpW\",\"use\":\"sig\"}]}", headers: {})
|
||||
post :create, redirect_uris: ["http://localhost"], client_name: "diaspora client",
|
||||
response_types: [], grant_types: [], application_type: "web", contacts: [],
|
||||
logo_uri: "http://example.com/logo.png", client_uri: "http://example.com/client",
|
||||
policy_uri: "http://example.com/policy", tos_uri: "http://example.com/tos",
|
||||
sector_identifier_uri: "http://example.com/uris", subject_type: "pairwise",
|
||||
token_endpoint_auth_method: "private_key_jwt",
|
||||
jwks_uri: "https://kentshikama.com/api/openid_connect/jwks.json"
|
||||
client_json = JSON.parse(response.body)
|
||||
expect(client_json["client_id"].length).to eq(32)
|
||||
expect(client_json["ppid"]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when redirect uri is missing" do
|
||||
it "should return a invalid_client_metadata error" do
|
||||
post :create, response_types: [], grant_types: [], application_type: "web", contacts: [],
|
||||
logo_uri: "http://example.com/logo.png", client_uri: "http://example.com/client",
|
||||
policy_uri: "http://example.com/policy", tos_uri: "http://example.com/tos"
|
||||
client_json = JSON.parse(response.body)
|
||||
expect(client_json["error"]).to have_content("invalid_client_metadata")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#find" do
|
||||
let!(:client) { FactoryGirl.create(:o_auth_application) }
|
||||
|
||||
context "when an OIDC client already exists" do
|
||||
it "should return a client id" do
|
||||
get :find, client_name: client.client_name
|
||||
client_id_json = JSON.parse(response.body)
|
||||
expect(client_id_json["client_id"]).to eq(client.client_id)
|
||||
end
|
||||
end
|
||||
|
||||
context "when an OIDC client doesn't already exist" do
|
||||
it "should return the appropriate error" do
|
||||
get :find, client_name: "random_name"
|
||||
client_id_json = JSON.parse(response.body)
|
||||
expect(client_id_json["error"]).to eq("Client with name random_name does not exist")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::OpenidConnect::DiscoveryController, type: :controller do
|
||||
describe "#webfinger" do
|
||||
before do
|
||||
get :webfinger, resource: "http://example.com/bob"
|
||||
end
|
||||
|
||||
it "should return a url to the openid-configuration" do
|
||||
json_body = JSON.parse(response.body)
|
||||
expect(json_body["links"].first["href"]).to eq(root_url)
|
||||
end
|
||||
|
||||
it "should return the resource in the subject" do
|
||||
json_body = JSON.parse(response.body)
|
||||
expect(json_body["subject"]).to eq("http://example.com/bob")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#configuration" do
|
||||
before do
|
||||
get :configuration
|
||||
end
|
||||
|
||||
it "should have the issuer as the root url" do
|
||||
json_body = JSON.parse(response.body)
|
||||
expect(json_body["issuer"]).to eq(root_url)
|
||||
end
|
||||
|
||||
it "should have the appropriate user info endpoint" do
|
||||
json_body = JSON.parse(response.body)
|
||||
expect(json_body["userinfo_endpoint"]).to eq(api_openid_connect_user_info_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::OpenidConnect::IdTokensController, type: :controller do
|
||||
describe "#jwks" do
|
||||
before do
|
||||
get :jwks
|
||||
end
|
||||
|
||||
it "should contain a public key that matches the internal private key" do
|
||||
json = JSON.parse(response.body).with_indifferent_access
|
||||
jwks = JSON::JWK::Set.new json[:keys]
|
||||
public_keys = jwks.map do |jwk|
|
||||
JSON::JWK.new(jwk).to_key
|
||||
end
|
||||
public_key = public_keys.first
|
||||
expect(Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY.to_s).to eq(public_key.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::OpenidConnect::UserApplicationsController, type: :controller do
|
||||
before do
|
||||
@app = FactoryGirl.create(:o_auth_application_with_xss)
|
||||
@user = FactoryGirl.create :user
|
||||
FactoryGirl.create :auth_with_read, user: @user, o_auth_application: @app
|
||||
sign_in :user, @user
|
||||
end
|
||||
|
||||
context "when try to XSS" do
|
||||
it "should not include XSS script" do
|
||||
get :index
|
||||
expect(response.body).to_not include("<script>alert(0);</script>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -310,6 +310,59 @@ FactoryGirl.define do
|
|||
|
||||
factory(:status, :parent => :status_message)
|
||||
|
||||
factory :o_auth_application, class: Api::OpenidConnect::OAuthApplication do
|
||||
client_name "Diaspora Test Client"
|
||||
redirect_uris %w(http://localhost:3000/)
|
||||
end
|
||||
|
||||
factory :o_auth_application_with_image, class: Api::OpenidConnect::OAuthApplication do
|
||||
client_name "Diaspora Test Client"
|
||||
redirect_uris %w(http://localhost:3000/)
|
||||
logo_uri "/assets/user/default.png"
|
||||
end
|
||||
|
||||
factory :o_auth_application_with_ppid, class: Api::OpenidConnect::OAuthApplication do
|
||||
client_name "Diaspora Test Client"
|
||||
redirect_uris %w(http://localhost:3000/)
|
||||
ppid true
|
||||
sector_identifier_uri "https://example.com/uri"
|
||||
end
|
||||
|
||||
factory :o_auth_application_with_ppid_with_specific_id, class: Api::OpenidConnect::OAuthApplication do
|
||||
client_name "Diaspora Test Client"
|
||||
redirect_uris %w(http://localhost:3000/)
|
||||
ppid true
|
||||
sector_identifier_uri "https://example.com/uri"
|
||||
end
|
||||
|
||||
factory :o_auth_application_with_multiple_redirects, class: Api::OpenidConnect::OAuthApplication do
|
||||
client_name "Diaspora Test Client"
|
||||
redirect_uris %w(http://localhost:3000/ http://localhost/)
|
||||
end
|
||||
|
||||
factory :o_auth_application_with_xss, class: Api::OpenidConnect::OAuthApplication do
|
||||
client_name "<script>alert(0);</script>"
|
||||
redirect_uris %w(http://localhost:3000/)
|
||||
end
|
||||
|
||||
factory :auth_with_read, class: Api::OpenidConnect::Authorization do
|
||||
o_auth_application
|
||||
user
|
||||
scopes %w(openid sub aud profile picture nickname name read)
|
||||
end
|
||||
|
||||
factory :auth_with_read_and_ppid, class: Api::OpenidConnect::Authorization do
|
||||
association :o_auth_application, factory: :o_auth_application_with_ppid
|
||||
user
|
||||
scopes %w(openid sub aud profile picture nickname name read)
|
||||
end
|
||||
|
||||
factory :auth_with_read_and_write, class: Api::OpenidConnect::Authorization do
|
||||
o_auth_application
|
||||
user
|
||||
scopes %w(openid sub aud profile picture nickname name read write)
|
||||
end
|
||||
|
||||
# Factories for the DiasporaFederation-gem
|
||||
|
||||
factory(:federation_person_from_webfinger, class: DiasporaFederation::Entities::Person) do
|
||||
|
|
|
|||
1
spec/fixtures/client_assertion_with_nonexistent_client_id.txt
vendored
Normal file
1
spec/fixtures/client_assertion_with_nonexistent_client_id.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.ewogIGF1ZDogWwogICAgaHR0cHM6Ly9rZW50c2hpa2FtYS5jb20vYXBpL29wZW5pZF9jb25uZWN0L2FjY2Vzc190b2tlbnMKICBdLAogIGlzczogMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2QsCiAganRpOiAwbWNycmVZSCwKICBleHA6IDE0NDMxNzA4OTEuMzk3NDU2LAogIGlhdDogMTQ0MzE3MDI5MS4zOTc0NTYsCiAgc3ViOiAxNGQ2OTJjZDUzZDljMWE5ZjQ2ZmQ2OWUwZTU3NDQzZAp9Cg.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQog
|
||||
1
spec/fixtures/client_assertion_with_nonexistent_kid.txt
vendored
Normal file
1
spec/fixtures/client_assertion_with_nonexistent_kid.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
ewogIGFsZzogUlMyNTYsCiAga2lkOiBpbnZhbGlkX2tpZAp9Cg.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.
|
||||
1
spec/fixtures/client_assertion_with_tampered_sig.txt
vendored
Normal file
1
spec/fixtures/client_assertion_with_tampered_sig.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQoe
|
||||
1
spec/fixtures/jwks.json
vendored
Normal file
1
spec/fixtures/jwks.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"keys": [{"use": "enc", "e": "AQAB", "d": "lZQv0_81euRLeUYU84Aodh0ar7ymDlzWP5NMra4Jklkb-lTBWkI-u4RMsPqGYyW3KHRoL_pgzZXSzQx8RLQfER6timRWb--NxMMKllZubByU3RqH2ooNuocJurspYiXkznPW1Mg9DaNXL0C2hwWPQHTeUVISpjgi5TCOV1ccWVyksFruya_VNL1CIByB-L0GL1rqbKv32cDwi2A3_jJa61cpzfLSIBe-lvCO6tuiDsR4qgJnUwnndQFwEI_4mLmD3iNWXrc8N-poleV8mBfMqBB5fWwy_ZTFCpmQ5AywGmctaik_wNhMoWuA4tUfY6_1LdKld-5Cjq55eLtuJjtvuQ", "n": "tx3Hjdbc19lkTiohbJrNj4jf2_90MEE122CRrwtFu6saDywKcG7Bi7w2FMAK2oTkuWfqhWRb5BEGmnSXdiCEPO5d-ytqP3nwlZXHaCDYscpP8bB4YLhvCn7R8Efw6gwQle24QPRP3lYoFeuUbDUq7GKA5SfaZUvWoeWjqyLIaBspKQsC26_Umx1E4IXLrMSL6nkRnrYcVZBAXrYCeTP1XtsV38_lZVJfHSaJaUy4PKaj3yvgm93EV2CXybPti7CCMXZ34VqqWiF64pQjZsPu3ZTr7ha_TTQq499-zYRQNDvIVsBDLQQIgrbctuGqj6lrXb31Jj3JIEYqH_4h5X9d0Q", "q": "1q-r-bmMFbIzrLK2U3elksZq8CqUqZxlSfkGMZuVkxgYMS-e4FPzEp2iirG-eO11aa0cpMMoBdTnVdGJ_ZUR93w0lGf9XnQAJqxP7eOsrUoiW4VWlWH4WfOiLgpO-pFtyTz_JksYYaotc_Z3Zy-Szw6a39IDbuYGy1qL-15oQuc", "p": "2lrYPppRbcQWu4LtWN6tOVUrtCOPv1eLTKTc7q8vCMcem1Ox5QFB7KnUtNZ5Ni7wnZUeVDfimNebtjNsGvDSrpgIlo9dEnFBQsQIkzZ2SkoYfgmF8hNdi6P-BfRjdgYouy4c6xAnGDgSMTip1YnPRyvbMaoYT9E_tEcBW5wOeoc", "kid": "a0", "kty": "RSA"}, {"use": "sig", "e": "AQAB", "d": "DodXDEtkovWWGsMEXYy_nEEMCWyROMOebCnCv0ey3i4M4bh2dmwqgz0e-IKQAFlGiMkidGL1lNbq0uFS04FbuRAR06dYw1cbrNbDdhrWFxKTd1L5D9p-x-gW-YDWhpI8rUGRa76JXkOSxZUbg09_QyUd99CXAHh-FXi_ZkIKD8hK6FrAs68qhLf8MNkUv63DTduw7QgeFfQivdopePxyGuMk5n8veqwsUZsklQkhNlTYQqeM1xb2698ZQcNYkl0OssEsSJKRjXt-LRPowKrdvTuTo2p--HMI0pIEeFs7H_u5OW3jihjvoFClGPynHQhgWmQzlQRvWRXh6FhDVqFeGQ", "n": "zfZzttF7HmnTYwSMPdxKs5AoczbNS2mOPz-tN1g4ljqI_F1DG8cgQDcN_VDufxoFGRERo2FK6WEN41LhbGEyP6uL6wW6Cy29qE9QZcvY5mXrncndRSOkNcMizvuEJes_fMYrmP_lPiC6kWiqItTk9QBWqJfiYKhCx9cSDXsBmJXn3KWQCVHvj1ANFWW0CWLMKlWN-_NMNLIWJN_pEAocTZMzxSFBK1b5_5J8ZS7hfWRF6MQmjsJcz2jzA21SQZNpre3kwnTGRSwo05sAS-TyeadDqQPWgbqX69UzcGq5irhzN8cpZ_JaTk3Y_uV6owanTZLVvCgdjaAnMYeZhb0KFw", "q": "5E5XKK5njT-zzRqqTeY2tgP9PJBACeaH_xQRHZ_1ydE7tVd7HdgdaEHfQ1jvKIHFkknWWOBAY1mlBc4YDirLShB_voShD8C-Hx3nF5sne5fleVfU-sZy6Za4B2U75PcE62oZgCPauOTAEm9Xuvrt5aMMovyzR8ecJZhm9bw7naU", "p": "5vJHCSM3H3q4RltYzENC9RyZZV8EUmpkv9moyguT5t-BUGA-T4W_FGIxzOPXRWOckIplKkoDKhavUeNmTZMCUcue0nkICSJpvNE4Nb2p5PZk_QqSdQNvCasQtdojEG0AmfVD85SU551CYxJdLdDFOqyK2entpMr8lhokem189As", "kid": "a1", "kty": "RSA"}, {"use": "sig", "crv": "P-256", "kty": "EC", "y": "Yg4IRzHBMIsuQK2Oz0Uukp1aNDnpdoyk6QBMtmfGHQQ", "x": "L0WUeVlc9r6YJd6ie9duvOU1RHwxSkJKA37IK9B4Bpc", "kid": "a2"}, {"use": "enc", "crv": "P-256", "kty": "EC", "y": "E6E6g5_ziIZvfdAoACctnwOhuQYMvQzA259aftPn59M", "x": "Yu8_BQE2L0f1MqnK0GumZOaj_77Tx70-LoudyRUnLM4", "kid": "a3"}]}
|
||||
1
spec/fixtures/valid_client_assertion.txt
vendored
Normal file
1
spec/fixtures/valid_client_assertion.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQog
|
||||
21
spec/integration/api/user_info_controller_spec.rb
Normal file
21
spec/integration/api/user_info_controller_spec.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
require "spec_helper"
|
||||
describe Api::OpenidConnect::UserInfoController do
|
||||
let!(:auth_with_read_and_ppid) { FactoryGirl.create(:auth_with_read_and_ppid) }
|
||||
let!(:access_token_with_read) { auth_with_read_and_ppid.create_access_token.to_s }
|
||||
|
||||
describe "#show" do
|
||||
before do
|
||||
@user = auth_with_read_and_ppid.user
|
||||
get api_openid_connect_user_info_path, access_token: access_token_with_read
|
||||
end
|
||||
|
||||
it "shows the info" do
|
||||
json_body = JSON.parse(response.body)
|
||||
expected_sub =
|
||||
@user.pairwise_pseudonymous_identifiers.find_or_create_by(identifier: "https://example.com/uri").guid
|
||||
expect(json_body["sub"]).to eq(expected_sub)
|
||||
expect(json_body["nickname"]).to eq(@user.name)
|
||||
expect(json_body["profile"]).to eq(File.join(AppConfig.environment.url, "people", @user.guid).to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::OpenidConnect::ProtectedResourceEndpoint, type: :request do
|
||||
let(:auth_with_read) { FactoryGirl.create(:auth_with_read) }
|
||||
let!(:access_token_with_read) { auth_with_read.create_access_token.to_s }
|
||||
let!(:expired_access_token) do
|
||||
access_token = auth_with_read.o_auth_access_tokens.create!
|
||||
access_token.expires_at = Time.zone.now - 100
|
||||
access_token.save
|
||||
access_token.bearer_token.to_s
|
||||
end
|
||||
let(:invalid_token) { SecureRandom.hex(32).to_s }
|
||||
|
||||
context "when valid access token is provided" do
|
||||
before do
|
||||
get api_openid_connect_user_info_path, access_token: access_token_with_read
|
||||
end
|
||||
|
||||
it "includes private in the cache-control header" do
|
||||
expect(response.headers["Cache-Control"]).to include("private")
|
||||
end
|
||||
end
|
||||
|
||||
context "when access token is expired" do
|
||||
before do
|
||||
get api_openid_connect_user_info_path, access_token: expired_access_token
|
||||
end
|
||||
|
||||
it "should respond with a 401 Unauthorized response" do
|
||||
expect(response.status).to be(401)
|
||||
end
|
||||
it "should have an auth-scheme value of Bearer" do
|
||||
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
|
||||
end
|
||||
end
|
||||
|
||||
context "when no access token is provided" do
|
||||
before do
|
||||
get api_openid_connect_user_info_path
|
||||
end
|
||||
|
||||
it "should respond with a 401 Unauthorized response" do
|
||||
expect(response.status).to be(401)
|
||||
end
|
||||
it "should have an auth-scheme value of Bearer" do
|
||||
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
|
||||
end
|
||||
end
|
||||
|
||||
context "when an invalid access token is provided" do
|
||||
before do
|
||||
get api_openid_connect_user_info_path, access_token: invalid_token
|
||||
end
|
||||
|
||||
it "should respond with a 401 Unauthorized response" do
|
||||
expect(response.status).to be(401)
|
||||
end
|
||||
|
||||
it "should have an auth-scheme value of Bearer" do
|
||||
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
|
||||
end
|
||||
|
||||
it "should contain an invalid_token error" do
|
||||
expect(response.body).to include("invalid_token")
|
||||
end
|
||||
end
|
||||
|
||||
context "when authorization has been destroyed" do
|
||||
before do
|
||||
auth_with_read.destroy
|
||||
get api_openid_connect_user_info_path, access_token: access_token_with_read
|
||||
end
|
||||
|
||||
it "should respond with a 401 Unauthorized response" do
|
||||
expect(response.status).to be(401)
|
||||
end
|
||||
|
||||
it "should have an auth-scheme value of Bearer" do
|
||||
expect(response.headers["WWW-Authenticate"]).to include("Bearer")
|
||||
end
|
||||
|
||||
it "should contain an invalid_token error" do
|
||||
expect(response.body).to include("invalid_token")
|
||||
end
|
||||
end
|
||||
end
|
||||
246
spec/lib/api/openid_connect/token_endpoint_spec.rb
Normal file
246
spec/lib/api/openid_connect/token_endpoint_spec.rb
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::OpenidConnect::TokenEndpoint, type: :request do
|
||||
let!(:client) { FactoryGirl.create(:o_auth_application_with_ppid) }
|
||||
let!(:auth) {
|
||||
Api::OpenidConnect::Authorization.find_or_create_by(
|
||||
o_auth_application: client, user: bob, redirect_uri: "http://localhost:3000/", scopes: ["openid"])
|
||||
}
|
||||
let!(:code) { auth.create_code }
|
||||
let!(:client_with_specific_id) { FactoryGirl.create(:o_auth_application_with_ppid_with_specific_id) }
|
||||
let!(:auth_with_specific_id) do
|
||||
client_with_specific_id.client_id = "14d692cd53d9c1a9f46fd69e0e57443e"
|
||||
client_with_specific_id.jwks = File.read(jwks_file_path)
|
||||
client_with_specific_id.save!
|
||||
Api::OpenidConnect::Authorization.find_or_create_by(
|
||||
o_auth_application: client_with_specific_id,
|
||||
user: bob, redirect_uri: "http://localhost:3000/", scopes: ["openid"])
|
||||
end
|
||||
let!(:code_with_specific_id) { auth_with_specific_id.create_code }
|
||||
|
||||
describe "the authorization code grant type" do
|
||||
context "when the authorization code is valid" do
|
||||
before do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
client_id: client.client_id, client_secret: client.client_secret,
|
||||
redirect_uri: "http://localhost:3000/", code: code
|
||||
end
|
||||
|
||||
it "should return a valid id token" do
|
||||
json = JSON.parse(response.body)
|
||||
encoded_id_token = json["id_token"]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
expected_guid = bob.pairwise_pseudonymous_identifiers.find_by(identifier: "https://example.com/uri").guid
|
||||
expect(decoded_token.sub).to eq(expected_guid)
|
||||
expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
|
||||
end
|
||||
|
||||
it "should return an id token with a kid" do
|
||||
json = JSON.parse(response.body)
|
||||
encoded_id_token = json["id_token"]
|
||||
kid = JSON::JWT.decode(encoded_id_token, :skip_verification).header[:kid]
|
||||
expect(kid).to eq("default")
|
||||
end
|
||||
|
||||
it "should return a valid access token" do
|
||||
json = JSON.parse(response.body)
|
||||
encoded_id_token = json["id_token"]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
access_token = json["access_token"]
|
||||
access_token_check_num = UrlSafeBase64.encode64(OpenSSL::Digest::SHA256.digest(access_token)[0, 128 / 8])
|
||||
expect(decoded_token.at_hash).to eq(access_token_check_num)
|
||||
end
|
||||
|
||||
it "should not allow code to be reused" do
|
||||
auth.reload
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
client_id: client.client_id, client_secret: client.client_secret,
|
||||
redirect_uri: "http://localhost:3000/", code: code
|
||||
expect(JSON.parse(response.body)["error"]).to eq("invalid_grant")
|
||||
end
|
||||
|
||||
it "should not allow a nil code" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
client_id: client.client_id, client_secret: client.client_secret,
|
||||
redirect_uri: "http://localhost:3000/", code: nil
|
||||
expect(JSON.parse(response.body)["error"]).to eq("invalid_request")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the authorization code is valid with jwt bearer" do
|
||||
before do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
redirect_uri: "http://localhost:3000/", code: code_with_specific_id,
|
||||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
client_assertion: File.read(valid_client_assertion_path)
|
||||
end
|
||||
|
||||
it "should return a valid id token" do
|
||||
json = JSON.parse(response.body)
|
||||
encoded_id_token = json["id_token"]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
expected_guid = bob.pairwise_pseudonymous_identifiers.find_by(identifier: "https://example.com/uri").guid
|
||||
expect(decoded_token.sub).to eq(expected_guid)
|
||||
expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
|
||||
end
|
||||
|
||||
it "should return a valid access token" do
|
||||
json = JSON.parse(response.body)
|
||||
encoded_id_token = json["id_token"]
|
||||
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
|
||||
Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
|
||||
access_token = json["access_token"]
|
||||
access_token_check_num = UrlSafeBase64.encode64(OpenSSL::Digest::SHA256.digest(access_token)[0, 128 / 8])
|
||||
expect(decoded_token.at_hash).to eq(access_token_check_num)
|
||||
end
|
||||
|
||||
it "should not allow code to be reused" do
|
||||
auth_with_specific_id.reload
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
client_id: client.client_id, client_secret: client.client_secret,
|
||||
redirect_uri: "http://localhost:3000/", code: code_with_specific_id
|
||||
expect(JSON.parse(response.body)["error"]).to eq("invalid_grant")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the authorization code is not valid" do
|
||||
it "should return an invalid grant error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
client_id: client.client_id, client_secret: client.client_secret, code: "123456"
|
||||
expect(response.body).to include "invalid_grant"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the client assertion is in an invalid format" do
|
||||
before do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
redirect_uri: "http://localhost:3000/", code: code_with_specific_id,
|
||||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
client_assertion: "invalid_client_assertion.random"
|
||||
end
|
||||
|
||||
it "should return an error" do
|
||||
expect(response.body).to include "invalid_request"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the client assertion is not matching with jwks keys" do
|
||||
before do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
redirect_uri: "http://localhost:3000/", code: code_with_specific_id,
|
||||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
client_assertion: File.read(client_assertion_with_tampered_sig_path)
|
||||
end
|
||||
|
||||
it "should return an error" do
|
||||
expect(response.body).to include "invalid_grant"
|
||||
end
|
||||
end
|
||||
|
||||
context "when kid doesn't exist in jwks keys" do
|
||||
before do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
redirect_uri: "http://localhost:3000/", code: code_with_specific_id,
|
||||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
client_assertion: File.read(client_assertion_with_nonexistent_kid_path)
|
||||
end
|
||||
|
||||
it "should return an error" do
|
||||
expect(response.body).to include "invalid_request"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the client is unregistered" do
|
||||
it "should return an error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code", code: auth.refresh_token,
|
||||
client_id: SecureRandom.hex(16).to_s, client_secret: client.client_secret
|
||||
expect(response.body).to include "invalid_client"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the client is unregistered with jwks keys" do
|
||||
before do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
redirect_uri: "http://localhost:3000/", code: code_with_specific_id,
|
||||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
client_assertion: File.read(client_assertion_with_nonexistent_client_id_path)
|
||||
end
|
||||
|
||||
it "should return an error" do
|
||||
expect(response.body).to include "invalid_request"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the code field is missing" do
|
||||
it "should return an invalid request error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code",
|
||||
client_id: client.client_id, client_secret: client.client_secret
|
||||
expect(response.body).to include "invalid_request"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the client_secret doesn't match" do
|
||||
it "should return an invalid client error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "authorization_code", code: auth.refresh_token,
|
||||
client_id: client.client_id, client_secret: "client.client_secret"
|
||||
expect(response.body).to include "invalid_client"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "an unsupported grant type" do
|
||||
it "should return an unsupported grant type error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "noexistgrant", username: "bob",
|
||||
password: "bluepin7", client_id: client.client_id, client_secret: client.client_secret, scope: "read"
|
||||
expect(response.body).to include "unsupported_grant_type"
|
||||
end
|
||||
end
|
||||
|
||||
describe "the refresh token grant type" do
|
||||
context "when the refresh token is valid" do
|
||||
it "should return an access token" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "refresh_token",
|
||||
client_id: client.client_id, client_secret: client.client_secret, refresh_token: auth.refresh_token
|
||||
json = JSON.parse(response.body)
|
||||
expect(response.body).to include "expires_in"
|
||||
expect(json["access_token"].length).to eq(64)
|
||||
expect(json["token_type"]).to eq("bearer")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the refresh token is not valid" do
|
||||
it "should return an invalid grant error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "refresh_token",
|
||||
client_id: client.client_id, client_secret: client.client_secret, refresh_token: "123456"
|
||||
expect(response.body).to include "invalid_grant"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the client is unregistered" do
|
||||
it "should return an error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "refresh_token", refresh_token: auth.refresh_token,
|
||||
client_id: SecureRandom.hex(16).to_s, client_secret: client.client_secret
|
||||
expect(response.body).to include "invalid_client"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the refresh_token field is missing" do
|
||||
it "should return an invalid request error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "refresh_token",
|
||||
client_id: client.client_id, client_secret: client.client_secret
|
||||
expect(response.body).to include "'refresh_token' required"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the client_secret doesn't match" do
|
||||
it "should return an invalid client error" do
|
||||
post api_openid_connect_access_tokens_path, grant_type: "refresh_token", refresh_token: auth.refresh_token,
|
||||
client_id: client.client_id, client_secret: "client.client_secret"
|
||||
expect(response.body).to include "invalid_client"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -59,6 +59,29 @@ def photo_fixture_name
|
|||
@photo_fixture_name = File.join(File.dirname(__FILE__), "fixtures", "button.png")
|
||||
end
|
||||
|
||||
def jwks_file_path
|
||||
@jwks_file = File.join(File.dirname(__FILE__), "fixtures", "jwks.json")
|
||||
end
|
||||
|
||||
def valid_client_assertion_path
|
||||
@valid_client_assertion = File.join(File.dirname(__FILE__), "fixtures", "valid_client_assertion.txt")
|
||||
end
|
||||
|
||||
def client_assertion_with_tampered_sig_path
|
||||
@client_assertion_with_tampered_sig = File.join(File.dirname(__FILE__), "fixtures",
|
||||
"client_assertion_with_tampered_sig.txt")
|
||||
end
|
||||
|
||||
def client_assertion_with_nonexistent_kid_path
|
||||
@client_assertion_with_nonexistent_kid = File.join(File.dirname(__FILE__), "fixtures",
|
||||
"client_assertion_with_nonexistent_kid.txt")
|
||||
end
|
||||
|
||||
def client_assertion_with_nonexistent_client_id_path
|
||||
@client_assertion_with_nonexistent_client_id = File.join(File.dirname(__FILE__), "fixtures",
|
||||
"client_assertion_with_nonexistent_client_id.txt")
|
||||
end
|
||||
|
||||
# Force fixture rebuild
|
||||
FileUtils.rm_f(Rails.root.join("tmp", "fixture_builder.yml"))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue