Skip to content
This repository has been archived by the owner on Mar 22, 2021. It is now read-only.

Support JWKs public keys #124

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
35 changes: 32 additions & 3 deletions app/model/knock/auth_token.rb
@@ -1,4 +1,7 @@
require 'uri'
require 'jwt'
require 'json/jwt'
require 'net/http'

module Knock
class AuthToken
Expand All @@ -7,7 +10,21 @@ class AuthToken

def initialize payload: {}, token: nil, verify_options: {}
if token.present?
@payload, _ = JWT.decode token.to_s, decode_key, true, options.merge(verify_options)
token_decode_key = decode_key

if token_decode_key.is_a?(JSON::JWK::Set)
@payload = JSON::JWT.decode(token.to_s, token_decode_key)
@options = options.merge(verify_options)

@options.each do |key, val|
next unless key.to_s =~ /verify/

JWT::Verify.send(key, @payload, @options) if val
end
else
@payload, _ = JWT.decode token.to_s, token_decode_key, true, options.merge(verify_options)
end

@token = token
else
@payload = claims.merge(payload)
Expand Down Expand Up @@ -35,12 +52,24 @@ def secret_key
end

def decode_key
Knock.token_public_key || secret_key
if Knock.token_public_key
if Knock.token_public_key =~ /^#{URI::Parser.new.make_regexp(['http', 'https'])}$/
# This means there's a JWK or JWKs public key that we need to fetch
JSON::JWK::Set.new(
JSON.parse( Net::HTTP.get( URI(Knock.token_public_key) ) )
)
else
Knock.token_public_key
end
else
secret_key
end
end

def options
verify_claims.merge({
algorithm: Knock.token_signature_algorithm
algorithm: Knock.token_signature_algorithm,
leeway: 0
})
end

Expand Down
2 changes: 2 additions & 0 deletions knock.gemspec
Expand Up @@ -19,8 +19,10 @@ Gem::Specification.new do |s|

s.add_dependency "rails", ">= 4.2"
s.add_dependency "jwt", "~> 1.5"
s.add_dependency "json-jwt", "~> 1.6"
s.add_dependency "bcrypt", "~> 3.1"

s.add_development_dependency "sqlite3", "~> 1.3"
s.add_development_dependency "timecop", "~> 0.8.0"
s.add_development_dependency "webmock", "~> 3.0"
end
2 changes: 2 additions & 0 deletions lib/generators/knock/install_generator.rb
@@ -1,3 +1,5 @@
require 'rails/generators'

module Knock
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path("../../templates", __FILE__)
Expand Down
2 changes: 2 additions & 0 deletions lib/generators/knock/token_controller_generator.rb
@@ -1,3 +1,5 @@
require 'rails/generators'

module Knock
class TokenControllerGenerator < Rails::Generators::Base
source_root File.expand_path("../../templates", __FILE__)
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/db/migrate/20150713101607_create_users.rb
@@ -1,4 +1,4 @@
class CreateUsers < ActiveRecord::Migration
class CreateUsers < ActiveRecord::Migration[4.2]
def change
create_table :users do |t|
t.string :email, unique: true, null: false
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/db/migrate/20160519075733_create_admins.rb
@@ -1,4 +1,4 @@
class CreateAdmins < ActiveRecord::Migration
class CreateAdmins < ActiveRecord::Migration[4.2]
def change
create_table :admins do |t|
t.string :email
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/db/migrate/20160522051816_create_vendors.rb
@@ -1,4 +1,4 @@
class CreateVendors < ActiveRecord::Migration
class CreateVendors < ActiveRecord::Migration[4.2]
def change
create_table :vendors do |t|
t.string :email
Expand Down
@@ -1,4 +1,4 @@
class CreateCompositeNameEntities < ActiveRecord::Migration
class CreateCompositeNameEntities < ActiveRecord::Migration[4.2]
def change
create_table :composite_name_entities do |t|
t.string :email
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/db/migrate/20161127203222_create_v1_users.rb
@@ -1,4 +1,4 @@
class CreateV1Users < ActiveRecord::Migration
class CreateV1Users < ActiveRecord::Migration[4.2]
def change
create_table :v1_users do |t|

Expand Down
37 changes: 37 additions & 0 deletions test/model/knock/auth_token_test.rb
@@ -1,6 +1,10 @@
require 'test_helper'
require 'jwt'
require 'timecop'
require 'webmock/minitest'

# Disable all remote connections
WebMock.disable_net_connect!

module Knock
class AuthTokenTest < ActiveSupport::TestCase
Expand All @@ -27,6 +31,39 @@ class AuthTokenTest < ActiveSupport::TestCase
assert_nothing_raised { AuthToken.new(token: token) }
end

test "decode RSA encoded tokens with JWKs from URL" do
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key

Knock.token_public_key = 'https://example.com/.well-known/jwks.json'
Knock.token_signature_algorithm = 'RS256'

stub_request(:get, Knock.token_public_key)
.to_return(body: JSON::JWK::Set.new(JSON::JWK.new(rsa_public)).to_json)

token = JSON::JWT.new({sub: "1"}).sign(JSON::JWK.new(rsa_private)).to_s

assert_nothing_raised { AuthToken.new(token: token) }
end

test "verify audience when token_audience is present with JWKs from URL" do
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key

Knock.token_audience = -> { 'bar' }
Knock.token_public_key = 'https://example.com/.well-known/jwks.json'
Knock.token_signature_algorithm = 'RS256'

stub_request(:get, Knock.token_public_key)
.to_return(body: JSON::JWK::Set.new(JSON::JWK.new(rsa_public)).to_json)

token = JSON::JWT.new({sub: "1"}).sign(JSON::JWK.new(rsa_private)).to_s

assert_raises(JWT::InvalidAudError) {
AuthToken.new token: token
}
end

test "encode tokens with RSA" do
rsa_private = OpenSSL::PKey::RSA.generate 2048
Knock.token_secret_signature_key = -> { rsa_private }
Expand Down