|
| 1 | +--- |
| 2 | +title: Basics of Authentication | GitHub API |
| 3 | +--- |
| 4 | + |
| 5 | +# Basics of Authentication |
| 6 | + |
| 7 | +* TOC |
| 8 | +{:toc} |
| 9 | + |
| 10 | +In this section, we're going to focus on the basics of authentication. Specifically, |
| 11 | +we're going to create a Ruby server (using [Sinatra][Sinatra]) that implements |
| 12 | +the [web flow][webflow] of an application in several different ways. |
| 13 | + |
| 14 | +Note: you can download the complete source code for this project [from the platform-samples repo](https://github.com/github/platform-samples/tree/master/api/ruby/basics-of-authentication). |
| 15 | + |
| 16 | +## Registering your app |
| 17 | + |
| 18 | +First, you'll need to [register your |
| 19 | +application](https://github.com/settings/applications/new) application. Every |
| 20 | +registered OAuth application is assigned a unique Client ID and Client Secret. |
| 21 | +The Client Secret should not be shared! That includes checking the string |
| 22 | +into your repository. |
| 23 | + |
| 24 | +You can fill out every piece of information however you like, except the |
| 25 | +**Authorization callback URL**. This is easily the most important piece to setting |
| 26 | +up your application. It's the callback URL that GitHub returns the user to after |
| 27 | +successful authentication. |
| 28 | + |
| 29 | +Since we're running a regular Sinatra server, the location of the local instance |
| 30 | +is set to `http://localhost:4567`. Let's fill in the callback URL as `http://localhost:4567/callback`. |
| 31 | + |
| 32 | +## Accepting user authorization |
| 33 | + |
| 34 | +Now, let's start filling out our simple server. Create a file called _server.rb_ and paste this into it: |
| 35 | + |
| 36 | + require 'sinatra' |
| 37 | + require 'rest_client' |
| 38 | + |
| 39 | + CLIENT_ID = ENV['GH_BASIC_CLIENT_ID'] |
| 40 | + CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID'] |
| 41 | + |
| 42 | + get '/' do |
| 43 | + erb :index, :locals => {:client_id => CLIENT_ID} |
| 44 | + end |
| 45 | + |
| 46 | +Your client ID and client secret keys come from [your application's configuration page](https://github.com/settings/applications). You should **never, _ever_** store these values in |
| 47 | +GitHub--or any other public place, for that matter. We recommend storing them as |
| 48 | +[environment variables][about env vars]--which is exactly what we've done here. |
| 49 | + |
| 50 | +Next, in _views/index.erb_, paste this content: |
| 51 | + |
| 52 | + |
| 53 | + <html> |
| 54 | + <head> |
| 55 | + </head> |
| 56 | + <body> |
| 57 | + <p>Well, hello there!</p> |
| 58 | + <p>We're going to now talk to the GitHub API. Ready? <a href="https://github.com/login/oauth/authorize?client_id=<%= client_id %>">Click here</a> to begin!</a></p> |
| 59 | + <p>If that link doesn't work, remember to provide your own <a href="http://developer.github.com/v3/oauth/#web-application-flow">Client ID</a>!</p> |
| 60 | + </body> |
| 61 | + </html> |
| 62 | + |
| 63 | +(If you're unfamiliar with how Sinatra works, we recommend [reading the Sinatra guide][Sinatra guide].) |
| 64 | + |
| 65 | +Obviously, you'll want to change `<your_client_id>` to match your actual Client ID. |
| 66 | + |
| 67 | +Navigate your browser to `http://localhost:4567`. After clicking on the link, you |
| 68 | +should be taken to GitHub, and presented with a dialog that looks something like this: |
| 69 | + |
| 70 | + |
| 71 | +If you trust yourself, click **Authorize App**. Wuh-oh! Sinatra spits out a |
| 72 | +`404` error. What gives?! |
| 73 | + |
| 74 | +Well, remember when we specified a Callback URL to be `callback`? We didn't provide |
| 75 | +a route for it, so GitHub doesn't know where to drop the user after they authorize |
| 76 | +the app. Let's fix that now! |
| 77 | + |
| 78 | +### Providing a callback |
| 79 | + |
| 80 | +In _server.rb_, add a route to specify what the callback should do: |
| 81 | + |
| 82 | + get '/callback' do |
| 83 | + # get temporary GitHub code... |
| 84 | + session_code = request.env['rack.request.query_hash']["code"] |
| 85 | + # ... and POST it back to GitHub |
| 86 | + result = RestClient.post("https://github.com/login/oauth/access_token", |
| 87 | + {:client_id => CLIENT_ID, |
| 88 | + :client_secret => CLIENT_SECRET, |
| 89 | + :code => session_code}, |
| 90 | + :accept => :json) |
| 91 | + access_token = JSON.parse(result)["access_token"] |
| 92 | + end |
| 93 | + |
| 94 | +After a successful app authentication, GitHub provides a temporary `code` value. |
| 95 | +You'll need to `POST` this code back to GitHub in exchange for an `access_token`. |
| 96 | +To simplify our GET and POST HTTP requests, we're using the [rest-client][REST Client]. |
| 97 | +Note that you'll probably never access the API through REST. For a more serious |
| 98 | +application, you should probably use [a library written in the language of your choice][libraries]. |
| 99 | + |
| 100 | +At last, with this access token, you'll be able to make authenticated requests as |
| 101 | +the logged in user: |
| 102 | + |
| 103 | + auth_result = RestClient.get("https://api.github.com/user", {:params => {:access_token => access_token}}) |
| 104 | + |
| 105 | + erb :basic, :locals => {:auth_result => auth_result} |
| 106 | + |
| 107 | +We can do whatever we want with our results. In this case, we'll just dump them straight into _basic.erb_: |
| 108 | + |
| 109 | + <p>Okay, here's a JSON dump:</p> |
| 110 | + <p> |
| 111 | + <p>Hello, <%= login %>! It looks like you're <%= hire_status %>.</p> |
| 112 | + </p> |
| 113 | + |
| 114 | +## Implementing "persistent" authentication |
| 115 | + |
| 116 | +It'd be a pretty bad model if we required users to log into the app every single |
| 117 | +time they needed to access the web page. For example, try navigating directly to |
| 118 | +`http://localhost:4567/basic`. You'll get an error. |
| 119 | + |
| 120 | +What if we could circumvent the entire |
| 121 | +"click here" process, and just _remember_ that, as log as the user's logged into |
| 122 | +GitHub, they should be able to access this application? Hold on to your hat, |
| 123 | +because _that's exactly what we're going to do_. |
| 124 | + |
| 125 | +Our little server above is rather simple. In order to wedge in some intelligent |
| 126 | +authentication, we're going to switch over to implementing [a Rack layer][rack guide] |
| 127 | +into our Sinatra app. On top of that, we're going to be using a middleware called |
| 128 | +[sinatra-auth-github][sinatra auth github] (which was written by a GitHubber). |
| 129 | +This will make authentication transparent to the user. |
| 130 | + |
| 131 | +After you run `gem install sinatra_auth_github`, create a file called _advanced_server.rb_, |
| 132 | +and paste these lines into it: |
| 133 | + |
| 134 | + require 'sinatra/auth/github' |
| 135 | + require 'rest_client' |
| 136 | + |
| 137 | + module Example |
| 138 | + class MyBasicApp < Sinatra::Base |
| 139 | + # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!! |
| 140 | + # Instead, set and test environment variables, like below |
| 141 | + # if ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET'] |
| 142 | + # CLIENT_ID = ENV['GITHUB_CLIENT_ID'] |
| 143 | + # CLIENT_SECRET = ENV['GITHUB_CLIENT_SECRET'] |
| 144 | + # end |
| 145 | + |
| 146 | + CLIENT_ID = ENV['GH_BASIC_CLIENT_ID'] |
| 147 | + CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID'] |
| 148 | + |
| 149 | + enable :sessions |
| 150 | + |
| 151 | + set :github_options, { |
| 152 | + :scopes => "user", |
| 153 | + :secret => CLIENT_SECRET, |
| 154 | + :client_id => CLIENT_ID, |
| 155 | + :callback_url => "/callback" |
| 156 | + } |
| 157 | + |
| 158 | + register Sinatra::Auth::Github |
| 159 | + |
| 160 | + get '/' do |
| 161 | + if !authenticated? |
| 162 | + authenticate! |
| 163 | + else |
| 164 | + access_token = github_user["token"] |
| 165 | + auth_result = RestClient.get("https://api.github.com/user", {:params => {:access_token => access_token, :accept => :json}, |
| 166 | + :accept => :json}) |
| 167 | + |
| 168 | + auth_result = JSON.parse(auth_result) |
| 169 | + |
| 170 | + erb :advanced, :locals => {:login => auth_result["login"], |
| 171 | + :hire_status => auth_result["hireable"] ? "hireable" : "not hireable"} |
| 172 | + end |
| 173 | + end |
| 174 | + |
| 175 | + get '/callback' do |
| 176 | + if authenticated? |
| 177 | + redirect "/" |
| 178 | + else |
| 179 | + authenticate! |
| 180 | + end |
| 181 | + end |
| 182 | + end |
| 183 | + end |
| 184 | + |
| 185 | +Much of the code should look familiar. For example, we're still using `RestClient.get` |
| 186 | +to call out to the GitHub API, and we're still passing our results to be renderend |
| 187 | +in an ERB template (this time, it's called `advanced`). Some of the other details-- |
| 188 | +like turning our app into a class that inherits from `Sinatra::Base`, are a result |
| 189 | +of inheriting from `sinatra/auth/github`, which written as [a Sinatra extension][sinatra extension]. |
| 190 | + |
| 191 | +Also, we now have a `github_user` object, which comes from `sinatra-auth-github`. The |
| 192 | +`token` key represents the same `access_token` we used during our simple server. |
| 193 | + |
| 194 | +`sinatra-auth-github` comes with quite a few options that you can customize. Here, |
| 195 | +we're establishing them through the `:github_options` symbol. Passing your client ID |
| 196 | +and client secret, and calling `register Sinatra::Auth::Github`, is everything you need |
| 197 | +to simplify your authentication. |
| 198 | + |
| 199 | +We must also create a _config.ru_ config file, which Rack will use for its configuration |
| 200 | +options: |
| 201 | + |
| 202 | + ENV['RACK_ENV'] ||= 'development' |
| 203 | + require "rubygems" |
| 204 | + require "bundler/setup" |
| 205 | + |
| 206 | + require File.expand_path(File.join(File.dirname(__FILE__), 'advanced_server')) |
| 207 | + |
| 208 | + run Example::MyBasicApp |
| 209 | + |
| 210 | +Next, create a file in _views_ called _advanced.erb_, and paste this markup into it: |
| 211 | + |
| 212 | + <html> |
| 213 | + <head> |
| 214 | + </head> |
| 215 | + <body> |
| 216 | + <p>Well, well, well, <%= login %>! It looks like you're <em>still</em> <%= hire_status %>!</p> |
| 217 | + </body> |
| 218 | + </html> |
| 219 | + |
| 220 | +From the command line, call `rackup -p 4567`, which starts up your |
| 221 | +Rack server on port `4567`--the same port we used when we had a simple Sinatra app. |
| 222 | +When you navigate to `http://localhost:4567`, the app calls `authenticate!`--another |
| 223 | +internal `sinatra-auth-github` method--which redirects you to `/callback`. `/callback` |
| 224 | +then sends us back to `/`, and since we've been authenticated, renders _advanced.erb_. |
| 225 | + |
| 226 | +We could completely simplify this roundtrip routing by simply changing our callback |
| 227 | +URL in GitHub to `/`. But, since both _server.rb_ and _advanced.rb_ are relying on |
| 228 | +the same callback URL, we've got to do a little bit of wonkiness to make it work. |
| 229 | + |
| 230 | +Also, if we had never authorized this Rack application to access our GitHub data, |
| 231 | +we would've seen the same confirmation dialog from earlier pop-up and warn us. |
| 232 | + |
| 233 | +If you'd like, you can play around with [yet another Sinatra-GitHub auth example][sinatra auth github test] |
| 234 | +available as a seperate project. |
| 235 | + |
| 236 | +[webflow]: http://developer.github.com/v3/oauth/#web-application-flow |
| 237 | +[Sinatra]: http://www.sinatrarb.com/ |
| 238 | +[about env vars]: http://en.wikipedia.org/wiki/Environment_variable#Getting_and_setting_environment_variables |
| 239 | +[Sinatra guide]: http://sinatra-book.gittr.com/#hello_world_application |
| 240 | +[REST Client]: https://github.com/archiloque/rest-client |
| 241 | +[libraries]: http://developer.github.com/v3/libraries/ |
| 242 | +[rack guide]: http://en.wikipedia.org/wiki/Rack_(web_server_interface) |
| 243 | +[sinatra auth github]: https://github.com/atmos/sinatra_auth_github |
| 244 | +[sinatra extension]: http://www.sinatrarb.com/extensions.html |
| 245 | +[sinatra auth github test]: https://github.com/atmos/sinatra-auth-github-test |
0 commit comments