Merge pull request #6095 from AugierLe42e/openid

OpenID Connect
This commit is contained in:
Jonne Haß 2016-01-06 12:20:18 +01:00
commit baeff22451
86 changed files with 3129 additions and 37 deletions

1
.gitignore vendored
View file

@ -20,6 +20,7 @@ vendor/cache/
config/database.yml
.rvmrc_custom
.rvmrc.local
config/oidc_key.pem
# Mailing list stuff
config/email_offset

View file

@ -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)

View file

@ -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"

View file

@ -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)

View file

@ -0,0 +1,5 @@
$(document).ready(function() {
$("#js-app-logo").error(function () {
$(this).attr("src", ImagePaths.get("user/default.png"));
});
});

View file

@ -45,3 +45,4 @@
//= require bootstrap-switch
//= require blueimp-gallery
//= require leaflet
//= require api/authorization_page

View file

@ -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';

View file

@ -13,6 +13,7 @@
@import "mobile/settings";
@import "mobile/stream_element";
@import "mobile/comments";
@import 'mobile/openid_connect_error_page';
@import 'typography';

View file

@ -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; }
}

View file

@ -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;
}
}

View file

@ -0,0 +1,7 @@
.api-error {
background-color: $light-grey;
box-shadow: $card-shadow;
margin-top: 20px;
h4 { text-align: center; }
}

View 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; }

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View 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

View file

@ -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

View 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

View 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

View file

@ -0,0 +1 @@
= render partial: "api/openid_connect/error/error"

View 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

View file

@ -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")

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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$/

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View 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

View 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

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.ewogIGF1ZDogWwogICAgaHR0cHM6Ly9rZW50c2hpa2FtYS5jb20vYXBpL29wZW5pZF9jb25uZWN0L2FjY2Vzc190b2tlbnMKICBdLAogIGlzczogMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2QsCiAganRpOiAwbWNycmVZSCwKICBleHA6IDE0NDMxNzA4OTEuMzk3NDU2LAogIGlhdDogMTQ0MzE3MDI5MS4zOTc0NTYsCiAgc3ViOiAxNGQ2OTJjZDUzZDljMWE5ZjQ2ZmQ2OWUwZTU3NDQzZAp9Cg.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQog

View file

@ -0,0 +1 @@
ewogIGFsZzogUlMyNTYsCiAga2lkOiBpbnZhbGlkX2tpZAp9Cg.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.

View file

@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQoe

1
spec/fixtures/jwks.json vendored Normal file
View 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"}]}

View file

@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQog

View 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

View file

@ -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

View 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

View file

@ -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"))