├── .rspec ├── lib ├── artsy-rack-auth-admin-only.rb └── artsy_auth │ ├── version.rb │ └── gravity.rb ├── .travis.yml ├── Rakefile ├── bin ├── setup └── console ├── .gitignore ├── Gemfile ├── spec ├── artsy │ └── rack │ │ └── auth │ │ └── admin │ │ └── only_spec.rb └── spec_helper.rb ├── README.md └── artsy-rack-auth-admin-only.gemspec /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/artsy-rack-auth-admin-only.rb: -------------------------------------------------------------------------------- 1 | require 'artsy_auth/gravity' 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.1.3 5 | before_install: gem install bundler -v 1.15.3 6 | -------------------------------------------------------------------------------- /lib/artsy_auth/version.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # 2 | 3 | module ArtsyAuth 4 | VERSION = '1.0.0'.freeze 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in artsy-rack-auth-admin-only.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /spec/artsy/rack/auth/admin/only_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Artsy::Rack::Auth::Admin::Only do 4 | it "has a version number" do 5 | expect(Artsy::Rack::Auth::Admin::Only::VERSION).not_to be nil 6 | end 7 | 8 | it "does something useful" do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "artsy/rack/auth/admin/only" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "artsy/rack/auth/admin/only" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArtsyAuth::Gravity 2 | 3 | A really simple authentication tool that uses the JWT to authenticate. 4 | 5 | ### Meta 6 | 7 | * __State:__ production 8 | * __Point People:__ [@orta](https://github.com/orta), [@wrgoldstein](https://github.com/wrgoldstein) 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'artsy-rack-auth-admin-only' 16 | ``` 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | ## Usage 23 | 24 | In your rack project, add the following ENV vars: 25 | 26 | ```sh 27 | GRAVITY_URL = # A gravity API instance like https://api.artsy.net/ 28 | APPLICATION_ID = # Your ClientApplication's ID 29 | APPLICATION_SECRET = # Your ClientApplication's secret 30 | APPLICATION_INTERNAL_SECRET = # Your ClientApplication's internal secret, you can get this via gravity console 31 | HOST = # Your site's public URL 32 | ``` 33 | 34 | Then inside the file where you're configuring your app, add: 35 | 36 | ```ruby 37 | require "artsy-rack-auth-admin-only" 38 | use ArtsyAuth::Gravity 39 | ``` 40 | 41 | ## Contributing 42 | 43 | Bug reports and pull requests are welcome on GitHub at https://github.com/artsy/artsy-rack-auth-admin-only. 44 | -------------------------------------------------------------------------------- /artsy-rack-auth-admin-only.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'artsy_auth/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'artsy-rack-auth-admin-only' 9 | spec.version = ArtsyAuth::VERSION 10 | spec.authors = ['Orta Therox', 'Will Goldstein'] 11 | spec.email = ['orta.therox@gmail.com', 'williamrgoldstein@gmail.com'] 12 | 13 | spec.summary = 'A simple gem for adding Rack based admin-only Oauth-credentials to Artsy apps.' 14 | spec.description = 'A simple gem for adding Rack based admin-only Oauth-credentials to Artsy apps.' 15 | spec.homepage = 'https://github.com/artsy/artsy-rack-auth-admin-only' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_runtime_dependency 'rack' 23 | spec.add_runtime_dependency 'jwt' 24 | 25 | spec.add_development_dependency 'bundler', '~> 1.15' 26 | spec.add_development_dependency 'rake', '~> 10.0' 27 | spec.add_development_dependency 'rspec', '~> 3.0' 28 | end 29 | -------------------------------------------------------------------------------- /lib/artsy_auth/gravity.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'open-uri' 3 | 4 | require 'jwt' 5 | require 'rack/auth/abstract/handler' 6 | require 'rack/auth/abstract/request' 7 | 8 | GRAVITY_URL = ENV['GRAVITY_URL'] 9 | APPLICATION_ID = ENV['APPLICATION_ID'] 10 | APPLICATION_SECRET = ENV['APPLICATION_SECRET'] 11 | APPLICATION_INTERNAL_SECRET = ENV['APPLICATION_INTERNAL_SECRET'] 12 | 13 | REDIRECT_URL = "#{ENV['HOST']}/auth".freeze 14 | OAUTH_REDIRECT = "#{GRAVITY_URL}/oauth2/authorize?client_id=#{APPLICATION_ID}&redirect_uri=#{REDIRECT_URL}&response_type=code".freeze 15 | 16 | COOKIE_EXP = Time.now + 7 * 24 * 60 * 60 17 | 18 | # Gravity auth code 19 | module ArtsyAuth 20 | # Generates a URL for the gravity oauth access code which the server gets 21 | # given after a user successfullly logs in. 22 | def self.oauth_url(code) 23 | query = [ 24 | "client_id=#{APPLICATION_ID}", 25 | "client_secret=#{APPLICATION_SECRET}", 26 | "redirect_uri=#{REDIRECT_URL}", 27 | "code=#{code}", 28 | 'grant_type=authorization_code' 29 | ] 30 | "#{GRAVITY_URL}/oauth2/access_token?#{query.join('&')}" 31 | end 32 | 33 | # An authentication library that uses the JWT and Artsy Oauth 34 | # to verify whether someone using a site is an admin. 35 | # 36 | class Gravity < Rack::Auth::AbstractHandler 37 | def call(env) 38 | return authorize(env) if env['REQUEST_PATH'] == '/auth' 39 | return @app.call(env) if valid?(env) 40 | unauthorized 41 | end 42 | 43 | private 44 | 45 | def valid?(env) 46 | cookies = Rack::Utils.parse_cookies_header(env['HTTP_COOKIE']) 47 | valid_access_token?(cookies['access_token']) 48 | end 49 | 50 | def valid_access_token?(access_token) 51 | return false if access_token.nil? 52 | jwt, = JWT.decode(access_token, APPLICATION_INTERNAL_SECRET) 53 | jwt['roles'].split(',').include? 'admin' 54 | end 55 | 56 | def authorize(env) 57 | query = Rack::Utils.parse_nested_query(env['QUERY_STRING']) 58 | code = query['code'] 59 | url = ArtsyAuth.oauth_url(code) 60 | response = open(url).read 61 | json = JSON.parse(response) 62 | return authorized(json) if valid_access_token?(json['access_token']) 63 | not_admin 64 | end 65 | 66 | def authorized(json) 67 | response = Rack::Response.new 68 | response.set_cookie('access_token', 69 | value: json['access_token'], 70 | path: '/', 71 | expires: COOKIE_EXP) 72 | response.redirect '/', 307 73 | response.finish 74 | end 75 | 76 | def unauthorized 77 | response = Rack::Response.new 78 | response.redirect OAUTH_REDIRECT, 307 79 | response.finish 80 | end 81 | 82 | def not_admin 83 | [403, { 'Content-Type' => 'text/plain' }, ['This is an Artsy-admin only page']] 84 | end 85 | end 86 | end 87 | --------------------------------------------------------------------------------