├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib └── omniauth │ ├── jwt.rb │ ├── jwt │ └── version.rb │ └── strategies │ └── jwt.rb ├── omniauth-jwt.gemspec └── spec ├── lib └── omniauth │ └── strategies │ └── jwt_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 1.9.3 5 | - jruby-19mode 6 | - rbx-19mode -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in omniauth-jwt.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard :rspec do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Michael Bleigh 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OmniAuth::JWT 2 | 3 | [![Build Status](https://travis-ci.org/mbleigh/omniauth-jwt.png)](https://travis-ci.org/mbleigh/omniauth-jwt) 4 | 5 | [JSON Web Token](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) (JWT) is a simple 6 | way to send verified information between two parties online. This can be useful as a mechanism for 7 | providing Single Sign-On (SSO) to an application by allowing an authentication server to send a validated 8 | claim and log the user in. This is how [Zendesk does SSO](https://support.zendesk.com/entries/23675367-Setting-up-single-sign-on-with-JWT-JSON-Web-Token-), 9 | for example. 10 | 11 | OmniAuth::JWT provides a clean, simple wrapper on top of JWT so that you can easily implement this kind 12 | of SSO either between your own applications or allow third parties to delegate authentication. 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile: 17 | 18 | gem 'omniauth-jwt' 19 | 20 | And then execute: 21 | 22 | $ bundle 23 | 24 | Or install it yourself as: 25 | 26 | $ gem install omniauth-jwt 27 | 28 | ## Usage 29 | 30 | You use OmniAuth::JWT just like you do any other OmniAuth strategy: 31 | 32 | ```ruby 33 | use OmniAuth::JWT, 'SHAREDSECRET', auth_url: 'http://example.com/login' 34 | ``` 35 | 36 | The first parameter is the shared secret that will be used by the external authenticator to verify 37 | that. You must also specify the `auth_url` option to tell the strategy where to redirect to log 38 | in. Other available options are: 39 | 40 | * **algorithm:** the algorithm to use to decode the JWT token. This is `HS256` by default but can 41 | be set to anything supported by [ruby-jwt](https://github.com/progrium/ruby-jwt) 42 | * **uid_claim:** this determines which claim will be used to uniquely identify the user. Defaults 43 | to `email` 44 | * **required_claims:** array of claims that are required to make this a valid authentication call. 45 | Defaults to `['name', 'email']` 46 | * **info_map:** array mapping claim values to info hash values. Defaults to mapping `name` and `email` 47 | to the same in the info hash. 48 | * **valid_within:** integer of how many seconds of time skew you will allow. Defaults to `nil`. If this 49 | is set, the `iat` claim becomes required and must be within the specified number of seconds of the 50 | current time. This helps to prevent replay attacks. 51 | 52 | ### Authentication Process 53 | 54 | When you authenticate through `omniauth-jwt` you can send users to `/auth/jwt` and it will redirect 55 | them to the URL specified in the `auth_url` option. From there, the provider must generate a JWT 56 | and send it to the `/auth/jwt/callback` URL as a "jwt" parameter: 57 | 58 | /auth/jwt/callback?jwt=ENCODEDJWTGOESHERE 59 | 60 | An example of how to do that in Sinatra: 61 | 62 | ```ruby 63 | require 'jwt' 64 | 65 | get '/login/sso/other-app' do 66 | # assuming the user is already logged in and this is available as current_user 67 | claims = { 68 | id: current_user.id, 69 | name: current_user.name, 70 | email: current_user.email, 71 | iat: Time.now.to_i 72 | } 73 | 74 | payload = JWT.encode(claims, ENV['SSO_SECRET']) 75 | redirect "http://other-app.com/auth/jwt/callback?jwt=#{payload}" 76 | end 77 | ``` 78 | 79 | ## Contributing 80 | 81 | 1. Fork it 82 | 2. Create your feature branch (`git checkout -b my-new-feature`) 83 | 3. Commit your changes (`git commit -am 'Add some feature'`) 84 | 4. Push to the branch (`git push origin my-new-feature`) 85 | 5. Create new Pull Request 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec -------------------------------------------------------------------------------- /lib/omniauth/jwt.rb: -------------------------------------------------------------------------------- 1 | require "omniauth/jwt/version" 2 | require "omniauth/strategies/jwt" -------------------------------------------------------------------------------- /lib/omniauth/jwt/version.rb: -------------------------------------------------------------------------------- 1 | module Omniauth 2 | module JWT 3 | VERSION = "0.0.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/jwt.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth' 2 | require 'jwt' 3 | 4 | module OmniAuth 5 | module Strategies 6 | class JWT 7 | class ClaimInvalid < StandardError; end 8 | 9 | include OmniAuth::Strategy 10 | 11 | args [:secret] 12 | 13 | option :secret, nil 14 | option :algorithm, 'HS256' 15 | option :uid_claim, 'email' 16 | option :required_claims, %w(name email) 17 | option :info_map, {"name" => "name", "email" => "email"} 18 | option :auth_url, nil 19 | option :valid_within, nil 20 | 21 | def request_phase 22 | redirect options.auth_url 23 | end 24 | 25 | def decoded 26 | @decoded ||= ::JWT.decode(request.params['jwt'], options.secret, options.algorithm) 27 | (options.required_claims || []).each do |field| 28 | raise ClaimInvalid.new("Missing required '#{field}' claim.") if !@decoded.key?(field.to_s) 29 | end 30 | raise ClaimInvalid.new("Missing required 'iat' claim.") if options.valid_within && !@decoded["iat"] 31 | raise ClaimInvalid.new("'iat' timestamp claim is too skewed from present.") if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within 32 | @decoded 33 | end 34 | 35 | def callback_phase 36 | super 37 | rescue ClaimInvalid => e 38 | fail! :claim_invalid, e 39 | end 40 | 41 | uid{ decoded[options.uid_claim] } 42 | 43 | extra do 44 | {:raw_info => decoded} 45 | end 46 | 47 | info do 48 | options.info_map.inject({}) do |h,(k,v)| 49 | h[k.to_s] = decoded[v.to_s] 50 | h 51 | end 52 | end 53 | end 54 | 55 | class Jwt < JWT; end 56 | end 57 | end -------------------------------------------------------------------------------- /omniauth-jwt.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'omniauth/jwt/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "omniauth-jwt" 8 | spec.version = Omniauth::JWT::VERSION 9 | spec.authors = ["Michael Bleigh"] 10 | spec.email = ["mbleigh@mbleigh.com"] 11 | spec.description = %q{An OmniAuth strategy to accept JWT-based single sign-on.} 12 | spec.summary = %q{An OmniAuth strategy to accept JWT-based single sign-on.} 13 | spec.homepage = "http://github.com/mbleigh/omniauth-jwt" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "rspec" 24 | spec.add_development_dependency "guard" 25 | spec.add_development_dependency "guard-rspec" 26 | spec.add_development_dependency "rack-test" 27 | 28 | spec.add_dependency "jwt" 29 | spec.add_dependency "omniauth", "~> 1.1" 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/omniauth/strategies/jwt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe OmniAuth::Strategies::JWT do 4 | let(:response_json){ MultiJson.load(last_response.body) } 5 | let(:args){ ['imasecret', {auth_url: 'http://example.com/login'}] } 6 | 7 | let(:app){ 8 | the_args = args 9 | Rack::Builder.new do |b| 10 | b.use Rack::Session::Cookie, secret: 'sekrit' 11 | b.use OmniAuth::Strategies::JWT, *the_args 12 | b.run lambda{|env| [200, {}, [(env['omniauth.auth'] || {}).to_json]]} 13 | end 14 | } 15 | 16 | context 'request phase' do 17 | it 'should redirect to the configured login url' do 18 | get '/auth/jwt' 19 | expect(last_response.status).to eq(302) 20 | expect(last_response.headers['Location']).to eq('http://example.com/login') 21 | end 22 | end 23 | 24 | context 'callback phase' do 25 | it 'should decode the response' do 26 | encoded = JWT.encode({name: 'Bob', email: 'steve@example.com'}, 'imasecret') 27 | get '/auth/jwt/callback?jwt=' + encoded 28 | expect(response_json["info"]["email"]).to eq("steve@example.com") 29 | end 30 | 31 | it 'should not work without required fields' do 32 | encoded = JWT.encode({name: 'Steve'}, 'imasecret') 33 | get '/auth/jwt/callback?jwt=' + encoded 34 | expect(last_response.status).to eq(302) 35 | end 36 | 37 | it 'should assign the uid' do 38 | encoded = JWT.encode({name: 'Steve', email: 'dude@awesome.com'}, 'imasecret') 39 | get '/auth/jwt/callback?jwt=' + encoded 40 | expect(response_json["uid"]).to eq('dude@awesome.com') 41 | end 42 | 43 | context 'with a :valid_within option set' do 44 | let(:args){ ['imasecret', {auth_url: 'http://example.com/login', valid_within: 300}] } 45 | 46 | it 'should work if the iat key is within the time window' do 47 | encoded = JWT.encode({name: 'Ted', email: 'ted@example.com', iat: Time.now.to_i}, 'imasecret') 48 | get '/auth/jwt/callback?jwt=' + encoded 49 | expect(last_response.status).to eq(200) 50 | end 51 | 52 | it 'should not work if the iat key is outside the time window' do 53 | encoded = JWT.encode({name: 'Ted', email: 'ted@example.com', iat: Time.now.to_i + 500}, 'imasecret') 54 | get '/auth/jwt/callback?jwt=' + encoded 55 | expect(last_response.status).to eq(302) 56 | end 57 | 58 | it 'should not work if the iat key is missing' do 59 | encoded = JWT.encode({name: 'Ted', email: 'ted@example.com'}, 'imasecret') 60 | get '/auth/jwt/callback?jwt=' + encoded 61 | expect(last_response.status).to eq(302) 62 | end 63 | end 64 | end 65 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) + "/../lib" 2 | require 'rack/test' 3 | 4 | require 'omniauth/jwt' 5 | OmniAuth.config.logger = Logger.new('/dev/null') 6 | # This file was generated by the `rspec --init` command. Conventionally, all 7 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 8 | # Require this file using `require "spec_helper"` to ensure that it is only 9 | # loaded once. 10 | # 11 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 12 | RSpec.configure do |config| 13 | config.treat_symbols_as_metadata_keys_with_true_values = true 14 | config.run_all_when_everything_filtered = true 15 | config.filter_run :focus 16 | 17 | include Rack::Test::Methods 18 | 19 | # Run specs in random order to surface order dependencies. If you find an 20 | # order dependency and want to debug it, you can fix the order by providing 21 | # the seed, which is printed after each run. 22 | # --seed 1234 23 | config.order = 'random' 24 | end 25 | --------------------------------------------------------------------------------