Commit baeff224 authored by Jonne Haß's avatar Jonne Haß

Merge pull request #6095 from AugierLe42e/openid

OpenID Connect
parents 439ea693 38439277
......@@ -20,6 +20,7 @@ vendor/cache/
config/database.yml
.rvmrc_custom
.rvmrc.local
config/oidc_key.pem
# Mailing list stuff
config/email_offset
......
......@@ -30,6 +30,15 @@ bind to an UNIX socket at `unix:tmp/diaspora.sock`. Please change your local
With the port to Bootstrap 3, app/views/terms/default.haml has a new structure. If you have created a customised app/views/terms/terms.haml or app/views/terms/terms.erb file, you will need to edit those files to base your customisations on the new default.haml file.
## API authentication
This release makes diaspora\* a OpenID Connect provider. This means you can authenticate to third parties with your diaspora\* account and let
them act as your diaspora* account on your behalf. This feature is still considered in early development, we still expect edge cases and advanced
features of the specificiation to not be handled correctly or be missing. But we expect a basic OpenID Connect compliant client to work. Please submit issues!
We will also most likely still change the authorization scopes we offer and started with a very minimal set.
Most work still required is on documentation as well as designing and implementing the data API for all of Diaspora's functionality.
Contributions are very welcome, the hard work is done!
## Refactor
* Improve bookmarklet [#5904](https://github.com/diaspora/diaspora/pull/5904)
* Update listen configuration to listen on unix sockets by default [#5974](https://github.com/diaspora/diaspora/pull/5974)
......
......@@ -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"
......
......@@ -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)
......
$(document).ready(function() {
$("#js-app-logo").error(function () {
$(this).attr("src", ImagePaths.get("user/default.png"));
});
});
......@@ -45,3 +45,4 @@
//= require bootstrap-switch
//= require blueimp-gallery
//= require leaflet
//= require api/authorization_page
@import 'perfect-scrollbar';
@import 'color-variables';
@import "bootstrap-complete.scss";
@import 'bootstrap-complete';
@import 'mixins';
......@@ -99,5 +99,11 @@
@import 'statistics';
/* gallery */
@import "blueimp-gallery";
@import "gallery";
@import 'blueimp-gallery';
@import 'gallery';
// settings
@import 'user_applications';
// OpenID Connect (API)
@import 'openid_connect_error_page';
......@@ -13,6 +13,7 @@
@import "mobile/settings";
@import "mobile/stream_element";
@import "mobile/comments";
@import 'mobile/openid_connect_error_page';
@import 'typography';
......
.landing { margin: -56px -20px 10px; }
.api-error {
background-color: $light-grey;
box-shadow: $card-shadow;
margin-top: 20px;
h4 { text-align: center; }
}
......@@ -30,11 +30,18 @@
}
}
#blocked_people {
.blocked_person {
border-bottom: 1px solid $border-grey;
margin-top: 0;
.avatar { max-width: 35px; }
.info { color: $text; }
.applications-page .applications-explanation {
margin-bottom: 15px;
}
.application-img {
margin: auto;
max-width: 150px;
text-align: center;
.entypo-browser {
font-size: 137px;
height: 160px;
margin-top: -45px;
}
}
.api-error {
background-color: $light-grey;
box-shadow: $card-shadow;
margin-top: 20px;
h4 { text-align: center; }
}
.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; }
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
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
# 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,