Commit a0093813 authored by Michael Tsyganov's avatar Michael Tsyganov Committed by Rémy Coutable

Support RSA and ECDSA algorithms in Omniauth JWT

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 5f1bb1a7
---
title: Support RSA and ECDSA algorithms in Omniauth JWT provider
merge_request: 23411
author: Michael Tsyganov
type: fixed
...@@ -548,15 +548,15 @@ production: &base ...@@ -548,15 +548,15 @@ production: &base
# app_id: 'YOUR_APP_ID', # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' } # app_secret: 'YOUR_APP_SECRET' }
# - { name: 'jwt', # - { name: 'jwt',
# app_secret: 'YOUR_APP_SECRET',
# args: { # args: {
# algorithm: 'HS256', # secret: 'YOUR_APP_SECRET',
# uid_claim: 'email', # algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
# required_claims: ["name", "email"], # uid_claim: 'email',
# info_map: { name: "name", email: "email" }, # required_claims: ['name', 'email'],
# auth_url: 'https://example.com/', # info_map: { name: 'name', email: 'email' },
# valid_within: null, # auth_url: 'https://example.com/',
# } # valid_within: 3600 # 1 hour
# }
# } # }
# - { name: 'saml', # - { name: 'saml',
# label: 'Our SAML Provider', # label: 'Our SAML Provider',
......
...@@ -10,7 +10,7 @@ providers. ...@@ -10,7 +10,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, - [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID Bitbucket, Facebook, Shibboleth, Crowd, Azure, Authentiq ID, and JWT
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta - [Okta](okta.md) Configure GitLab to sign in using Okta
......
...@@ -26,15 +26,15 @@ JWT will provide you with a secret key for you to use. ...@@ -26,15 +26,15 @@ JWT will provide you with a secret key for you to use.
```ruby ```ruby
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ name: 'jwt', { name: 'jwt',
app_secret: 'YOUR_APP_SECRET',
args: { args: {
algorithm: 'HS256', secret: 'YOUR_APP_SECRET',
uid_claim: 'email', algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
required_claims: ["name", "email"], uid_claim: 'email',
info_maps: { name: "name", email: "email" }, required_claims: ['name', 'email'],
auth_url: 'https://example.com/', info_maps: { name: 'name', email: 'email' },
valid_within: nil, auth_url: 'https://example.com/',
} valid_within: 3600 # 1 hour
}
} }
] ]
``` ```
...@@ -43,15 +43,15 @@ JWT will provide you with a secret key for you to use. ...@@ -43,15 +43,15 @@ JWT will provide you with a secret key for you to use.
``` ```
- { name: 'jwt', - { name: 'jwt',
app_secret: 'YOUR_APP_SECRET',
args: { args: {
algorithm: 'HS256', secret: 'YOUR_APP_SECRET',
uid_claim: 'email', algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
required_claims: ["name", "email"], uid_claim: 'email',
info_map: { name: "name", email: "email" }, required_claims: ['name', 'email'],
auth_url: 'https://example.com/', info_map: { name: 'name', email: 'email' },
valid_within: null, auth_url: 'https://example.com/',
} valid_within: 3600 # 1 hour
}
} }
``` ```
...@@ -60,7 +60,7 @@ JWT will provide you with a secret key for you to use. ...@@ -60,7 +60,7 @@ JWT will provide you with a secret key for you to use.
1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL. 1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL.
1. Save the configuration file. 1. Save the configuration file.
1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively. installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a JWT icon below the regular sign in form. On the sign in page there should now be a JWT icon below the regular sign in form.
...@@ -68,5 +68,5 @@ Click the icon to begin the authentication process. JWT will ask the user to ...@@ -68,5 +68,5 @@ Click the icon to begin the authentication process. JWT will ask the user to
sign in and authorize the GitLab application. If everything goes well, the user sign in and authorize the GitLab application. If everything goes well, the user
will be redirected to GitLab and will be signed in. will be redirected to GitLab and will be signed in.
[reconfigure GitLab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../restart_gitlab.md#installations-from-source [restart GitLab]: ../restart_gitlab.md#installations-from-source
# frozen_string_literal: true # frozen_string_literal: true
require 'omniauth' require 'omniauth'
require 'openssl'
require 'jwt' require 'jwt'
module OmniAuth module OmniAuth
...@@ -37,7 +38,19 @@ module OmniAuth ...@@ -37,7 +38,19 @@ module OmniAuth
end end
def decoded def decoded
@decoded ||= ::JWT.decode(request.params['jwt'], options.secret, options.algorithm).first secret =
case options.algorithm
when *%w[RS256 RS384 RS512]
OpenSSL::PKey::RSA.new(options.secret).public_key
when *%w[ES256 ES384 ES512]
OpenSSL::PKey::EC.new(options.secret).tap { |key| key.private_key = nil }
when *%w(HS256 HS384 HS512)
options.secret
else
raise NotImplementedError, "Unsupported algorithm: #{options.algorithm}"
end
@decoded ||= ::JWT.decode(request.params['jwt'], secret, true, { algorithm: options.algorithm }).first
(options.required_claims || []).each do |field| (options.required_claims || []).each do |field|
raise ClaimInvalid, "Missing required '#{field}' claim" unless @decoded.key?(field.to_s) raise ClaimInvalid, "Missing required '#{field}' claim" unless @decoded.key?(field.to_s)
...@@ -45,7 +58,7 @@ module OmniAuth ...@@ -45,7 +58,7 @@ module OmniAuth
raise ClaimInvalid, "Missing required 'iat' claim" if options.valid_within && !@decoded["iat"] raise ClaimInvalid, "Missing required 'iat' claim" if options.valid_within && !@decoded["iat"]
if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within.to_i
raise ClaimInvalid, "'iat' timestamp claim is too skewed from present" raise ClaimInvalid, "'iat' timestamp claim is too skewed from present"
end end
......
...@@ -4,12 +4,10 @@ describe OmniAuth::Strategies::Jwt do ...@@ -4,12 +4,10 @@ describe OmniAuth::Strategies::Jwt do
include Rack::Test::Methods include Rack::Test::Methods
include DeviseHelpers include DeviseHelpers
context '.decoded' do context '#decoded' do
let(:strategy) { described_class.new({}) } subject { described_class.new({}) }
let(:timestamp) { Time.now.to_i } let(:timestamp) { Time.now.to_i }
let(:jwt_config) { Devise.omniauth_configs[:jwt] } let(:jwt_config) { Devise.omniauth_configs[:jwt] }
let(:key) { JWT.encode(claims, jwt_config.strategy.secret) }
let(:claims) do let(:claims) do
{ {
id: 123, id: 123,
...@@ -18,19 +16,55 @@ describe OmniAuth::Strategies::Jwt do ...@@ -18,19 +16,55 @@ describe OmniAuth::Strategies::Jwt do
iat: timestamp iat: timestamp
} }
end end
let(:algorithm) { 'HS256' }
let(:secret) { jwt_config.strategy.secret }
let(:private_key) { secret }
let(:payload) { JWT.encode(claims, private_key, algorithm) }
before do before do
allow_any_instance_of(OmniAuth::Strategy).to receive(:options).and_return(jwt_config.strategy) subject.options[:secret] = secret
allow_any_instance_of(Rack::Request).to receive(:params).and_return({ 'jwt' => key }) subject.options[:algorithm] = algorithm
expect_next_instance_of(Rack::Request) do |rack_request|
expect(rack_request).to receive(:params).and_return('jwt' => payload)
end
end end
it 'decodes the user information' do ECDSA_NAMED_CURVES = {
result = strategy.decoded 'ES256' => 'prime256v1',
'ES384' => 'secp384r1',
'ES512' => 'secp521r1'
}.freeze
expect(result["id"]).to eq(123) {
expect(result["name"]).to eq("user_example") OpenSSL::PKey::RSA => %w[RS256 RS384 RS512],
expect(result["email"]).to eq("user@example.com") OpenSSL::PKey::EC => %w[ES256 ES384 ES512],
expect(result["iat"]).to eq(timestamp) String => %w[HS256 HS384 HS512]
}.each do |private_key_class, algorithms|
algorithms.each do |algorithm|
context "when the #{algorithm} algorithm is used" do
let(:algorithm) { algorithm }
let(:secret) do
if private_key_class == OpenSSL::PKey::RSA
private_key_class.generate(2048)
.to_pem
elsif private_key_class == OpenSSL::PKey::EC
private_key_class.new(ECDSA_NAMED_CURVES[algorithm])
.tap { |key| key.generate_key! }
.to_pem
else
private_key_class.new(jwt_config.strategy.secret)
end
end
let(:private_key) { private_key_class ? private_key_class.new(secret) : secret }
it 'decodes the user information' do
result = subject.decoded
expect(result).to eq(claims.stringify_keys)
end
end
end
end end
context 'required claims is missing' do context 'required claims is missing' do
...@@ -43,7 +77,7 @@ describe OmniAuth::Strategies::Jwt do ...@@ -43,7 +77,7 @@ describe OmniAuth::Strategies::Jwt do
end end
it 'raises error' do it 'raises error' do
expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid) expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end end
end end
...@@ -57,11 +91,12 @@ describe OmniAuth::Strategies::Jwt do ...@@ -57,11 +91,12 @@ describe OmniAuth::Strategies::Jwt do
end end
before do before do
jwt_config.strategy.valid_within = Time.now.to_i # Omniauth config values are always strings!
subject.options[:valid_within] = 2.days.to_s
end end
it 'raises error' do it 'raises error' do
expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid) expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end end
end end
...@@ -76,11 +111,12 @@ describe OmniAuth::Strategies::Jwt do ...@@ -76,11 +111,12 @@ describe OmniAuth::Strategies::Jwt do
end end
before do before do
jwt_config.strategy.valid_within = 2.seconds # Omniauth config values are always strings!
subject.options[:valid_within] = 2.seconds.to_s
end end
it 'raises error' do it 'raises error' do
expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid) expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment