Skip to content

Commit

Permalink
Merge "CFID 325 - Ruby login server for the UAA"
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldsa authored and Gerrit Code Review committed Jun 19, 2012
2 parents ae43346 + 0ab4378 commit c22e65f
Show file tree
Hide file tree
Showing 99 changed files with 1,026 additions and 0 deletions.
8 changes: 8 additions & 0 deletions samples/ruby-login-server/Gemfile
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"
34 changes: 34 additions & 0 deletions samples/ruby-login-server/Gemfile.lock
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
86 changes: 86 additions & 0 deletions samples/ruby-login-server/README.md
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
12 changes: 12 additions & 0 deletions samples/ruby-login-server/config.ru
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
234 changes: 234 additions & 0 deletions samples/ruby-login-server/login.rb
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
Loading

0 comments on commit c22e65f

Please sign in to comment.