-
Notifications
You must be signed in to change notification settings - Fork 24
Home
friend-oauth2 is an OAuth2 workflow for Friend.
OAuth2 allows you to send users to a third party site for authentication. They are then redirected to a URL you choose on your site with an access token. This token can be used by the server to obtain user-authorized metadata about the user from the third party provider.
Working examples have been implemented for:
##Table of Contents
-
Installation and Usage
- 1) Add the library to your project
- 2) Obtain third party credentials
- 3) Set up Friend and your main Ring handler stack.
- 4) Provide your OAuth2 client-id and client-secret to friend-oauth2
- 5) Set up your credential-fn to find user metadata
- 6) Configure the third party's OAuth2 URLs
- 7) Add the workflow to your Friend handler
- 8) Add restrictions to your routes and handlers
- Configuration Reference
- Testing
- To-do:
- License
In project.clj:
[com.cemerick/friend "0.2.0"]
[friend-oauth2 "0.1.1"]
As a security best practice, it is strongly recommended that you use Environ or similar to store your credentials in environment variables, rather than hardcode any credentials into the source code itself. In this way, if an attacker steals your code, they still won't be able to use your credentials.
To use Environ, also add:
[environ "0.5.0"]
to your project.clj.
To be able to use friend-oauth2, you must obtain site credentials from
the third party providers you wish to authenticte against, then
provide those credentials to friend-oauth2. The exact procedure for
obtaining the credentials varies by provider; consult their docs for
details. You will be given a :client-id
and a
:client-secret
. (These are for your server code, not for your
clients - you are a client of the third party provdier.)
Besides friend-oauth2, you must also set up Friend itself. This is typically done near where your main Ring app stack is being constructed. friend-oauth2 is then given to Friend as a possible workflow. It's easy to use other workflows besides friend-oauth2 in the same app, too. See the Friend docs for more information on setting up Friend itself.
Near where your Ring stack is being constructed, add the following requires:
(ns your.ns.here
(:require [cemerick.friend :as friend]
[friend-oauth2.workflow :as oauth2]
[friend-oauth2.util :refer [format-config-uri]]
[environ.core :refer [env]])
Using Environ and Google APIs OAuth2 as an example:
(def client-config
{:client-id (env :friend-oauth2-client-id)
:client-secret (env :friend-oauth2-client-secret)
:callback {:domain "http://localhost:8090" ;; replace this for production with the appropriate site URL
:path "/oauth2callback"}})
In ~/.lein/profiles.clj
, add your actual credentials (N.B. this file
is not checked into source control! It should only be present on
your local machine, and should be protected from other users as it
contains sensitive passwords):
{:user {:env {
:friend-oauth2-client-id "12345.apps.googleusercontent.com"
:friend-oauth2-client-secret "xyz"
}}
In production, these can be set at the ops level as ordinary environment variables (with underscores instead of dashes):
export FRIEND_OAUTH2_CLIENT_ID=12345.apps.googleusercontent.com
export FRIEND_OAUTH2_CLIENT_secret=xyz
./bin/run-my-app.sh
If the user login succeeds on the third party site, you will get a
token back in your credential-fn
uniquely identifying the user. You
can use this token to look up user metadata from the third party
service. What metadata you can look up is provider-specific. You can
also use it to create accounts in your own system, assign roles to the
user, and so on, as your particular app requires..
(defn credential-fn
"Upon successful authentication with the third party, Friend calls
this function with the user's token. This function is responsible for
translating that into a Friend identity map with at least the :identity
and :roles keys. How you decide what roles to grant users is up to you;
you could e.g. look them up in a database.
You can also return nil here if you decide that the token provided
is invalid. This could be used to implement e.g. banning users.
This example code just automatically assigns anyone who has
authenticated with the third party the nominal role of ::user."
[token]
{:identity token
:roles #{::user}})
credential-fn
behaves slightly differently differently in
friend-oauth2 than in other workflows: it allows you to intercept the
access token at the end of the 3rd-party authentication process and
inject your own functionality. This is an artifact of OAuth2's
original purpose as a protocol for obtaining 3rd-party
authorization of third party resources, not authentication of
user identity as such. It therefore does not return the user metadata
you probably find interesting; just a token. However, nowadays almost
everyone uses OAuth primarily for authentication rather than
authorization, so it is typical to find e.g. local database
interaction in your own system, or third party metadata lookup here.
Create the following data structure, given with Google as the example. Consult the examples and the third party's documentation for the URLs to use with other providers.
(def uri-config
{:authentication-uri {:url "https://accounts.google.com/o/oauth2/auth"
:query {:client_id (:client-id client-config)
:response_type "code"
:redirect_uri (format-config-uri client-config)
:scope "email"}}
:access-token-uri {:url "https://accounts.google.com/o/oauth2/token"
:query {:client_id (:client-id client-config)
:client_secret (:client-secret client-config)
:grant_type "authorization_code"
:redirect_uri (format-config-uri client-config)}}})
Finally, create an OAuth2 workflow and add it to your Friend handler, then add the Friend handler to your Ring stack:
(def friend-config
{:allow-anon? true
:workflows [(oauth2/workflow
{:client-config client-config
:uri-config uri-config
:credential-fn credential-fn})
;; Optionally add other workflows here...
]})
(def app
(-> my-ring-app
(friend/authenticate friend-config)
handler/site))
Access control is performed by Friend as usual. See Friend's docs for the full details.
For example, to allow access to a certain Compojure route only to users who are logged in and have the ::oauth2-user
role:
(GET "/authlink" request
(friend/authorize #{::oauth2-user} "Authorized page."))
Or, you can protect an entire Ring substack with Friend's wrap-authorize
function:
(friend/wrap-authorize admin-routes #{::administrator})
Check out the friend-oauth2 examples and refer to the Friend README for more information.
The map passed to oauth2/workflow
accepts various optional arguments, described below. See also the example handlers for working examples.
-
client-config
holds the basic information which changes from app to app, regardless of the provider::client-id
,:client-secret
, and the application's callback url. - The
authentication-uri
map holds the provider-specific configuration for the initial redirect to the OAuth2 provider (the user-facing GET request). - The
access-token-uri
map holds the provider-specific configuration for the access_token request, after the code is returned from the previous redirect (a server-to-server POST request). -
access-token-parsefn
is a provider-specific function which parses the access_token response and returns just the access_token (see below.)
If your OAuth2 provider does not follow the RFC
(http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.1, ("in
the entity body of the HTTP response using the "application/json"
media type as defined by [RFC4627]") then you can pass in a custom
function to parse the access_token response. Note that there is an
alternate function (get-access-token-from-params
) supplied to handle
the common case where an access_token is provided as parameters in the
callback request. Simply set the :access-token-parsefn get-access-token-from-params
See the
Facebook and Github examples for reference.
friend-oauth2 uses Midje (https://github.com/marick/Midje) for testing. You can run all the tests by starting up a repl, running use 'midje.repl
and running autotest
, or run lein midje :autotest
on the command line.
- Move client_id/client_secret to Authorization header (necessary? Good for security or immaterial? Does FB support this?) (http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-2.3)
Distributed under the MIT License (http://dd.mit-license.org/)