- Publicity: Public Only All
authorize-procs.tcl
Support for oauth authorization API
This file defines the following Objects and Classes: ::xo::oauth::GitHub, ::xo::Authorize, ::xo::oauth::GitHub, ::xo::Authorize
- Location:
- packages/xooauth/tcl/authorize-procs.tcl
- Author:
- Gustaf Neumann
Procedures in this file
- Class ::xo::Authorize (public)
- Class ::xo::oauth::GitHub (public)
- xo::Authorize instproc login_url (public)
- xo::Authorize instproc logout (public)
- xo::Authorize instproc name (public)
- xo::Authorize instproc perform_login (public)
- xo::oauth::GitHub instproc get_user_data (public)
- xo::oauth::GitHub instproc logout_url (public)
Detailed information
Class ::xo::Authorize (public)
::nx::Class ::xo::Authorize
Base class to support OAuth authorization API
- Testcases:
- No testcase defined.
Class ::xo::oauth::GitHub (public)
::nx::Class ::xo::oauth::GitHub
Tailored OAuth handler for GitHub
- Testcases:
- No testcase defined.
xo::Authorize method login_url (public)
<instance of xo::Authorize> login_url [ -return_url return_url ] \ [ -login login ]
Returns the URL for log-in
- Switches:
- -return_url (optional)
- -login (optional)
- Testcases:
- No testcase defined.
xo::Authorize method logout (public)
<instance of xo::Authorize> logout
Perform logout operation from oauth in the background (i.e. without a redirect) when the logout_url is nonempty.
- Testcases:
- No testcase defined.
xo::Authorize method name (public)
<instance of xo::Authorize> name
- Returns:
- instance name
- Testcases:
- No testcase defined.
xo::Authorize method perform_login (public)
<instance of xo::Authorize> perform_login [ -token token ] \ [ -state state ]
Get the provided claims from the identity provider and perform an OpenACS login, when the user exists. In case the user does not exist, create it optionally (when "create_not_registered_users" is activated. When the user is created, and dotlrn is installed, the new user might be added optionally as a dotlrn user with the role as specified in "create_with_dotlrn_role".
- Switches:
- -token (optional)
- -state (optional)
- Testcases:
- No testcase defined.
xo::oauth::GitHub method get_user_data (public)
<instance of xo::oauth::GitHub> get_user_data [ -token token ] \ [ -required_fields required_fields ]
Get user data based on the temporary code (passed via "-token") provided by GitHub. First, convert the temporary code into an access_token, and use this to get the user data.
- Switches:
- -token (optional)
- -required_fields (optional, defaults to
" {email email} {name name} "
)- Testcases:
- No testcase defined.
xo::oauth::GitHub method logout_url (public)
<instance of xo::oauth::GitHub> logout_url \ [ -return_url return_url ]
Returns the URL for logging out. E.g., GitHub has no logout, so provide simply a redirect URL (maybe, we should logout from the application?)
- Switches:
- -return_url (optional)
- Testcases:
- No testcase defined.
Content File Source
::xo::library doc { Support for oauth authorization API @author Gustaf Neumann } ::xo::library require rest-procs # # Namespace of oauth handlers # namespace eval ::xo::oauth {} namespace eval ::xo { ########################################################### # # xo::Authorize class # ########################################################### nx::Class create ::xo::Authorize -superclasses ::xo::REST { # # Base class to support OAuth authorization API # :property {pretty_name} :property {base_url} :property {responder_url} :property {after_successful_login_url /pvt/} :property {login_failure_url /} :property {create_not_registered_users:switch false} :property {create_with_dotlrn_role ""} :property {scope} :property {debug:switch false} :method qualified {partial_url} { return [util_current_location]$partial_url } :method encoded_state {{-return_url ""}} { set state [::xo::oauth::nonce] append state . [ns_base64urlencode $return_url] return $state } :method decoded_state {state} { lassign [split $state .] nonce encoded_url return [list \ nonce $nonce \ return_url [ns_base64urldecode $encoded_url]] } :public method login_url { {-return_url ""} {-login} } { # # Returns the URL for log-in # set base ${:base_url}/authorize set client_id ${:client_id} set scope ${:scope} set state [:encoded_state -return_url $return_url] set redirect_uri [:qualified ${:responder_url}] return [export_vars -no_empty -base $base { client_id redirect_uri state scope login }] } :method redeem_code {code} { set client_id ${:client_id} set client_secret ${:client_secret} set redirect_uri [:qualified ${:responder_url}] set url [export_vars -no_empty -base ${:base_url}/access_token { client_id client_secret code redirect_uri }] set data [ns_http run $url] if {[dict get $data status] ne 200} { dict set data error oacs-cant_redeem_code dict set data error_description $data } else { set form_data [ns_set array [ns_parsequery [dict get $data body]]] ns_log notice "[self] redeem_code formdata has keys: [lsort [dict keys $form_data]]" if {![dict exists $form_data access_token]} { if {[dict exists $form_data error]} { dict set data error [dict get $form_data error] dict set data error_description [dict get $form_data error_description] } else { dict set data error oacs-no_access_token dict set data error_description $form_data } } else { dict set data access_token [dict get $form_data access_token] } } ns_log notice "[self] redeem_code returns $data" return $data } :public method logout {} { # # Perform logout operation from oauth in the background # (i.e. without a redirect) when the logout_url is # nonempty. # set url [:logout_url] if {$url ne ""} { ns_http run $url } } :public method name {} { # # @return instance name # return [expr {[info exists :pretty_name] ? ${:pretty_name} : [namespace tail [self]]}] } :method lookup_user_id {-email} -returns integer { set user_id [party::get_by_email -email [string tolower $email]] if {$user_id eq ""} { # # Here one could do some more checks or alternative lookups # } return [expr {$user_id eq "" ? 0 : $user_id}] } :method required_fields {} { return [expr {${:create_not_registered_users} ? "email given_name family_name" : "email"}] } :method get_required_fields { {-claims:required} {-mapped_fields:required} } { # # Check, if required fields are provided in the claims and # perform the name mapping between what was provided from # the identity provided and what we need in OpenACS. # set result "" set fields {} foreach pair $mapped_fields { lassign $pair field target if {[dict exists $claims $field]} { dict set fields $target [dict get $claims $field] } } dict set result fields $fields foreach field [:required_fields] { if {![dict exists $fields $field] || [dict get $fields $field] in {"" "null"} } { set not_enough_data $field break } } if {[info exists not_enough_data]} { ns_log warning "[self] get_user_data: not enough data:" \ $not_enough_data "is missing" \ "($field -> $target, claims: $claims)" dict set result error oacs-not_enough_data } return $result } :method record_oauth_registration {user_id} { # # Record the fact that this user_id was created via an # OAuth identity provider. # set auth_obj [self] db_dml _ { INSERT INTO xooauth_authorized_users (user_id, auth_obj) VALUES (:user_id, :auth_obj) } } :method register_new_user { {-first_names} {-last_name} {-email} } -returns integer { # # Register the user and return the user_id. In case, the # registration of the new user fails, raise an exception. # # not tested # db_transaction { set user_info(first_names) $first_names set user_info(last_name) $last_name if {![util_email_unique_p $email]} { error "Email is not unique: $email" } set user_info(email) $email array set creation_info [auth::create_local_account \ -authority_id [auth::authority::local] \ -username $email \ -array user_info] if {$creation_info(creation_status) ne "ok"} { set errorMsg "" error [append errorMsg "Error when creating user: " \ $creation_info(creation_status) " " \ $creation_info(element_messages)] } set user_id $creation_info(user_id) :record_oauth_registration $user_id if {[apm_package_installed_p dotlrn] && ${:create_with_dotlrn_role} ne ""} { # # We have DotLRN installed, and we want to create # for this register object the new users in the # provided role. Note that one can define # different instances of this class behaving # differently. # dotlrn::user_add \ -type ${:create_with_dotlrn_role} \ -can_browse=1 \ -id $email \ -user_id $user_id ::permission::grant \ -party_id $user_id \ -object_id [dotlrn::get_package_id] \ -privilege read_private_data } } on_error { ns_log error "OAuth Login (Error during user creation): $errmsg ($email)" error "OAuth user creation failed: $errmsg" } return $user_id } :public method perform_login {-token {-state ""}} { # # Get the provided claims from the identity provider and # perform an OpenACS login, when the user exists. # # In case the user does not exist, create it optionally (when # "create_not_registered_users" is activated. # # When the user is created, and dotlrn is installed, the # new user might be added optionally as a dotlrn user with # the role as specified in "create_with_dotlrn_role". # set data [:get_user_data -token $token] if {[dict exists $data error]} { # # There was already an error in the steps leading to # this. # ns_log warning "[self] OAuth login failed:" \ [dict get $data error] "\n$data" } elseif {![dict exists $data email]} { # # No error and no email in result... actually, this # should not happen. # dict set data error oacs-no_email_in_result ns_log warning "OAuth login failed strangely: " \ [dict get $data error] "\n$data" } else { dict set data decoded_state [:decoded_state $state] set user_id [:lookup_user_id -email [dict get $data email]] if {!${:debug} && $user_id == 0 && ${:create_not_registered_users} } { try { :register_new_user \ -first_names [dict get $data given_name] \ -last_name [dict get $data family_name] \ -email [dict get $data email] } on ok {result} { set user_id $result } on error {errorMsg} { dict set data error oacs-register_failed dict set data error_description $errorMsg } } dict set data user_id $user_id if {$user_id != 0} { # # The lookup of the user_id was successful. We can # login as this user.... but only, when no "debug" # is activated. # if {!${:debug}} { ad_user_login -external_registry [self] $user_id } } else { # # For the time being, just report data back to the # calling script. # dict set data error "oacs-no_such_user" } } return $data } } #################################################################### # # ::xo::oauth::GitHub class # #################################################################### nx::Class create ::xo::oauth::GitHub -superclasses ::xo::Authorize { # # Tailored OAuth handler for GitHub # :property {pretty_name "GitHub"} :property {base_url https://github.com/login/oauth} :property {responder_url /oauth/github-login-handler} :property {scope {read:user user:email}} :method get_api_data {access_token} { set data [ns_http run \ -headers [ns_set create query Authorization "Bearer $access_token"] \ https://api.github.com/user] if {[dict get $data status] ne 200} { dict set result error oacs-cant_get_api_data dict set result error_description $data } else { set json_body [dict get $data body] dict set result claims [:json_to_dict $json_body] } ns_log notice "[self] get_api_data $access_token returns $result" return $result } :public method get_user_data { -token {-required_fields { {email email} {name name} }} } { # # Get user data based on the temporary code (passed via # "-token") provided by GitHub. First, convert the # temporary code into an access_token, and use this to get # the user data. # set data [:redeem_code $token] ns_log notice "[self] redeem_code returned keys: [lsort [dict keys $data]]" set result $data if {[dict exists $data access_token]} { # # When we received the access_token, we have no error # so far. # set access_token [dict get $data access_token] #ns_log notice "[self] redeemed form data: $access_token" set result [:get_api_data $access_token] ns_log notice "[self] get_user_data: get_api_data contains error:" \ [dict exists $result error] if {![dict exists $result error]} { set data [:get_required_fields \ -claims [dict get $result claims] \ -mapped_fields { {email email} {name name} }] if {[dict exists $data error]} { set result [dict merge $data $result] } else { set result [dict merge $result [dict get $data fields]] # # We have still to split up "name" into its # components, since GitHub does not provide # the exactly needed fields. Actually, we need # this just for creating a new user_id, so it # might not be always needed. # set first_names [join [lrange [dict get $result name] 0 end-1] " "] set last_name [lindex [dict get $result name] end] dict set result first_names $first_names dict set result last_name $last_name } } } ns_log notice "[self] get_user_data returns $result" return $result } :public method logout_url { {-return_url ""} } { # # Returns the URL for logging out. E.g., GitHub has no # logout, so provide simply a redirect URL (maybe, we # should logout from the application?) # return $return_url } } # # In general, it might be possible, that a user is identified over # multiple OAuth identity providers, so the unique constraint # might be too strong. For now, we add only users to this table, # which were created from this authority - such that the unique # constraint holds. # ::xo::db::require table xooauth_authorized_users [subst { user_id {integer references users(user_id) on delete cascade} auth_obj {character varying(255)} }] ::xo::db::require index -table xooauth_authorized_users -col user_id -unique true } ::xo::library source_dependent # # Local variables: # mode: tcl # tcl-indent-level: 2 # indent-tabs-mode: nil # End