-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge "CFID 325 - Ruby login server for the UAA"
- Loading branch information
Showing
99 changed files
with
1,026 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
source :rubygems | ||
|
||
gem "sinatra" | ||
gem "rest-client" | ||
gem "yajl-ruby" | ||
gem "sequel" | ||
gem "ruby-openid" | ||
gem "thin" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
GEM | ||
remote: http://rubygems.org/ | ||
specs: | ||
daemons (1.1.8) | ||
eventmachine (0.12.10) | ||
mime-types (1.18) | ||
rack (1.4.1) | ||
rack-protection (1.2.0) | ||
rack | ||
rest-client (1.6.7) | ||
mime-types (>= 1.16) | ||
ruby-openid (2.1.8) | ||
sequel (3.34.1) | ||
sinatra (1.3.2) | ||
rack (~> 1.3, >= 1.3.6) | ||
rack-protection (~> 1.2) | ||
tilt (~> 1.3, >= 1.3.3) | ||
thin (1.3.1) | ||
daemons (>= 1.0.9) | ||
eventmachine (>= 0.12.6) | ||
rack (>= 1.0.0) | ||
tilt (1.3.3) | ||
yajl-ruby (0.8.3) | ||
|
||
PLATFORMS | ||
ruby | ||
|
||
DEPENDENCIES | ||
rest-client | ||
ruby-openid | ||
sequel | ||
sinatra | ||
thin | ||
yajl-ruby |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
# Login Sample Application | ||
|
||
This login application is a sample for how you can set up your own custom login user interface using the UAA as a backend Identity Provider. The application also supports different openid identity providers. The application is written in ruby and uses the sinatra framework. You may choose to embed it along with your code to provide a customized look and feel for the login interface using the UAA as an identity provider and a token server to issue Oauth access tokens. | ||
|
||
## Quick start | ||
|
||
Start your UAA | ||
|
||
$ git clone git@github.com:cloudfoundry/uaa.git | ||
$ cd uaa | ||
$ mvn install | ||
$ mvn tomcat:run | ||
|
||
Verify that the uaa has started by going to http://localhost:8080/uaa | ||
|
||
Start the sample login application | ||
|
||
$ cd samples/ruby-login-server | ||
$ bundle install | ||
$ bundle exec thin start | ||
>> Using rack adapter | ||
I, [2012-06-14T14:54:46.273942 #5369] INFO -- : Using token server http://localhost:8080/uaa | ||
>> Thin web server (v1.3.1 codename Triple Espresso) | ||
>> Maximum connections set to 1024 | ||
>> Listening on 0.0.0.0:3000, CTRL+C to stop | ||
|
||
You can start the oauth flow with a HTTP GET request to http://localhost:3000/oauth/authorize?client_id=app&response_type=code&scope=openid&redirect_uri=http://foo.com | ||
|
||
Login with the pre-created UAA user/password of "marissa/koala" | ||
|
||
## Customizing the application | ||
|
||
### Using a different token server | ||
|
||
The back end UAA token server URL can be customized by setting the UAA_TOKEN_SERVER environment variable | ||
This application also uses the login client to support logging in with authenticated email addresses from OpenID providers. The secret for the login client can be customized by | ||
setting the LOGIN_CLIENT_SECRET environment variable | ||
|
||
For details on the login client, please refer to the UAA. | ||
|
||
Each of these variables default to the values for a locally hosted UAA. | ||
|
||
### Customizing the user interface | ||
|
||
There are two ruby templates that can be customized for look and feel to match your application. These are login.erb used to display the login interface and confirm.erb used to display the authorization confirmation page. | ||
|
||
### Logging | ||
|
||
Before using this as a sample, please configure the application logging to ensure that no confidential information is logged. | ||
|
||
## Playing with the entire flow | ||
|
||
You may write a simple sinatra client application to test the end to end flow as follows. This client also uses application defaults for a locally hosted UAA. It logs the user into the UAA and displays a UAA access token if the login succeeds | ||
|
||
require 'sinatra' | ||
require 'yajl' | ||
require 'restclient' | ||
require 'base64' | ||
|
||
LOGIN_SERVER_URL = ENV['LOGIN_SERVER_URL'] || "http://localhost:3000" | ||
UAA_TOKEN_SERVER = ENV['UAA_TOKEN_SERVER'] || "http://localhost:8080/uaa" | ||
CLIENT_SECRET = ENV['CLIENT_SECRET'] || "appclientsecret" | ||
|
||
get '/' do | ||
"<html><body><a href=\"#{LOGIN_SERVER_URL}/oauth/authorize?client_id=app&response_type=code&scope=openid&redirect_uri=#{request.scheme}://#{request.host_with_port}/done\"\">Login</a></body></html>" | ||
end | ||
|
||
get '/done' do | ||
code = params[:code] | ||
$logger = Logger.new(STDOUT) | ||
RestClient.log = $logger | ||
response = RestClient.post("#{UAA_TOKEN_SERVER}/oauth/token", {"grant_type" => "authorization_code", "code" => "#{code}", "redirect_uri" => "#{request.scheme}://#{request.host_with_port}/done"}, {:accept => :json, :authorization => "Basic #{Base64.strict_encode64("app:#{CLIENT_SECRET}")}"}) \ | ||
{|response, request, result, &block| response} | ||
puts "#{response.body.inspect}" | ||
|
||
begin | ||
access_token = Yajl::Parser.new.parse(response.body)["access_token"] | ||
decoded_token = Base64.decode64(access_token.split('.')[1]) | ||
"Access Token from UAA is #{access_token} \ | ||
<br /><br />Decoded token is #{decoded_token} \ | ||
<br /><br /><a href=\"#{LOGIN_SERVER_URL}/logout\">Logout</a>" | ||
rescue => e | ||
puts "#{e.backtrace}" | ||
"Could not fetch access token" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
$:.unshift(".") | ||
|
||
require 'login' | ||
require 'openid_login' | ||
|
||
map "/openid" do | ||
run OpenIdLoginApplication.new | ||
end | ||
|
||
map "/" do | ||
run LoginApplication.new | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
require 'sinatra/base' | ||
require 'restclient' | ||
require 'yajl' | ||
require 'logger' | ||
require 'base64' | ||
|
||
# The LoginApplication class handles the oauth | ||
# authorization flow for the CloudFoundry UAA | ||
class LoginApplication < Sinatra::Base | ||
enable :sessions | ||
|
||
# URL of the uaa token server | ||
UAA_TOKEN_SERVER = ENV['UAA_TOKEN_SERVER'] || "http://localhost:8080/uaa" | ||
# Client secret of the login client. The login client allows this | ||
# application to authenticate to the token endpoint to get an access token | ||
# for a pre-authenticated email address (for the case when a pre-authenticated | ||
# email address is received by this application from an OpenID provider) | ||
LOGIN_CLIENT_SECRET = ENV['LOGIN_CLIENT_SECRET'] || "loginsecret" | ||
|
||
# Handles requests to the /login endpoint. | ||
# If an authenticated user session already exists with the authorization server, | ||
# the user is redirected to a confimation page | ||
get '/login' do | ||
pass unless params[:email].nil? | ||
|
||
# If there is already a session with the uaa, the user has been authenticated | ||
if uaa_session?(request).nil? | ||
erb :login | ||
else | ||
redirect '/confirm' | ||
end | ||
end | ||
|
||
# The start of the oauth flow for the client application | ||
get '/oauth/authorize' do | ||
if params.nil? || params.empty? | ||
halt 404 | ||
end | ||
|
||
# Store the request parameters in session | ||
[:client_id, :response_type, :scope, :redirect_uri, :state].each{|param| session[param] = params[param.to_s]} | ||
$logger.debug("Saving request parameters #{session.inspect}") | ||
|
||
# Redirect the user to the login page if no session | ||
# is available with the uaa | ||
if uaa_session?(request).nil? | ||
redirect '/login' | ||
else | ||
redirect '/confirm' | ||
end | ||
end | ||
|
||
# Common login flow for incoming username and password credentials | ||
# or incoming authenticated email addresses | ||
login = lambda do | ||
username = request[:username] | ||
password = request[:password] | ||
|
||
uaa_response = nil | ||
unless username.nil? && password.nil? | ||
# Post the credentials and get a session with the uaa. Save the uaa cookie | ||
uaa_response = post("#{UAA_TOKEN_SERVER}/login.do", \ | ||
{"username" => username, "password" => password}) | ||
$logger.debug "#{uaa_response.headers.inspect}" | ||
else | ||
# Get the email address from the openid provider and ask for authorization directly | ||
email = session[:authenticated_email] | ||
$logger.debug "authenticated email address #{email}" | ||
# Post the credentials and get a session with the uaa. Save the uaa cookie | ||
uaa_response = post_to_authorize({"login" => "#{Yajl::Encoder.encode("username" => email)}"}, \ | ||
{:authorization => "bearer #{login_access_token()}"}) | ||
$logger.debug "#{uaa_response.headers.inspect}" | ||
end | ||
|
||
# If the response from the UAA is a redirect ending in error=true, authentication has failed | ||
if uaa_response.headers[:location] =~ /\?error=true/ | ||
redirect '/login?error=true' | ||
end | ||
|
||
if uaa_response.nil? | ||
$logger.debug("Could not establish session with the uaa") | ||
redirect '/login?error=true' | ||
end | ||
|
||
# Maintain the cookie with the uaa | ||
uaa_cookie = uaa_response.cookies["JSESSIONID"] | ||
$logger.debug "UAA Cookie #{uaa_cookie}" | ||
response.set_cookie("uaa_cookie", uaa_cookie) | ||
|
||
redirect '/confirm' | ||
end | ||
|
||
# A similar authorization flow is implemented for the uaa as well as openid flows | ||
get '/authenticatedlogin', &login | ||
post '/login', &login | ||
|
||
# Handles authorization confirmation flow | ||
get '/confirm' do | ||
cookie = uaa_session?(request) | ||
$logger.debug("Current values in session #{session.inspect}") | ||
unless cookie.nil? | ||
[:client_id, :response_type, :scope, :redirect_uri].each do |parameter| \ | ||
unless session[:parameter].nil? | ||
halt 400, "Invalid request. User authenticated but unable to find \"#{parameter}\" parameter \ | ||
from request. Please see README for directions." | ||
end | ||
end | ||
# Maintaining the UAA cookie from the successful login to the uaa, | ||
# post to the /oauth/authorize endpoint to get the confirmation data | ||
request_params = {"client_id" => session[:client_id], | ||
"response_type" => session[:response_type], | ||
"scope" => session[:scope], | ||
"redirect_uri" => session[:redirect_uri]} | ||
request_params["state"] = session[:state] unless session[:state].nil? | ||
uaa_response = post_to_authorize(request_params, {:cookies => { :JSESSIONID => cookie}}) | ||
$logger.debug "#{uaa_response.inspect}" | ||
case uaa_response.code | ||
when 200 | ||
confirmation_info = Yajl::Parser.new.parse(uaa_response.body) | ||
$logger.debug "#{confirmation_info.inspect}" | ||
session[:confirm_key] = (confirmation_info["options"]["confirm"]["key"] \ | ||
if confirmation_info["options"] && confirmation_info["options"]["confirm"]) || "user_oauth_approval" | ||
|
||
erb :confirm, :locals => {:client_id => confirmation_info["authorizationRequest"]["clientId"], \ | ||
:scopes => confirmation_info["authorizationRequest"]["scope"]} | ||
when 302 | ||
# Access confirmation not required, get the code. | ||
$logger.debug "#{uaa_response.headers[:location]}" | ||
|
||
redirect uaa_response.headers[:location] | ||
else | ||
halt 500, "error from the token server #{uaa_response.inspect}" | ||
end | ||
else | ||
halt 500, "invalid state" | ||
end | ||
end | ||
|
||
# User confirms / denies authorization | ||
post '/confirm' do | ||
choice = params[:choice] | ||
$logger.debug "#{choice}" | ||
|
||
target = '/login' | ||
|
||
# User confirms authorization | ||
if choice == "yes" | ||
cookie = request.cookies['uaa_cookie'] | ||
|
||
# Post confirmation to the uaa and redirect to the target | ||
request_params = {"client_id" => session[:client_id], | ||
"response_type" => session[:response_type], | ||
"scope" => session[:scope], | ||
"redirect_uri" => session[:redirect_uri], | ||
session[:confirm_key] => "true"} | ||
request_params[:state] = session[:state] unless session[:state].nil? | ||
uaa_response = post_to_authorize(request_params, {:cookies => { :JSESSIONID => cookie}}) | ||
$logger.debug "#{uaa_response.headers[:location]}" | ||
target = uaa_response.headers[:location] | ||
else | ||
cleanup(response) | ||
end | ||
|
||
redirect target | ||
end | ||
|
||
get '/logout' do | ||
cleanup(response) | ||
end | ||
|
||
helpers do | ||
def uaa_session?(request) | ||
cookie = request.cookies['uaa_cookie'] | ||
$logger.debug("Found uaa session cookie #{cookie}") | ||
unless cookie.nil? | ||
$logger.debug "cookie value #{cookie}" | ||
response = post_to_authorize(nil, {:cookies => { :JSESSIONID => cookie}}) | ||
$logger.debug("uaa_session #{response.headers.inspect}") | ||
if response.code == 302 && response.headers[:location] =~ /\/login/ | ||
cookie = nil | ||
end | ||
end | ||
cookie | ||
end | ||
|
||
def cleanup(response) | ||
response.delete_cookie('uaa_cookie') | ||
session.clear | ||
end | ||
|
||
def post_to_authorize(request_params, headers) | ||
headers = headers.merge(:accept => :json) | ||
|
||
$logger.debug("Headers to post to authorize #{headers}") | ||
|
||
post("#{UAA_TOKEN_SERVER}/oauth/authorize", request_params, headers) | ||
end | ||
|
||
def login_access_token | ||
# Get an access token for the login client | ||
login_response = post("#{UAA_TOKEN_SERVER}/oauth/token", \ | ||
{"response_type" => "token", "grant_type" => "client_credentials"}, \ | ||
{:accept => :json, :authorization => "Basic #{Base64.strict_encode64("login:#{LOGIN_CLIENT_SECRET}")}"}) | ||
$logger.debug "#{login_response.body.inspect}" | ||
access_token = Yajl::Parser.new.parse(login_response.body)["access_token"] | ||
end | ||
|
||
def post(url, content, headers = nil) | ||
begin | ||
response = RestClient.post(url, content, headers) \ | ||
{|response, request, result, &block| response} | ||
rescue => e | ||
$logger.error("Error connecting to #{url}, #{e.backtrace}") | ||
halt 500, "UAA unavailable." | ||
end | ||
end | ||
end | ||
|
||
configure :development do | ||
$logger = Logger.new(STDOUT) | ||
RestClient.log = $logger | ||
$logger.info("Using token server #{UAA_TOKEN_SERVER}") | ||
end | ||
|
||
configure :production do | ||
$logger = Logger.new(STDOUT) | ||
$logger.info("Using token server #{UAA_TOKEN_SERVER}") | ||
end | ||
|
||
get '/' do | ||
redirect '/login' | ||
end | ||
|
||
end |
Oops, something went wrong.