require 'spec_helper' describe OpenIDConnect::ResponseObject::IdToken do let(:klass) { OpenIDConnect::ResponseObject::IdToken } let(:id_token) { klass.new attributes } let(:attributes) { required_attributes } let(:ext) { 10.minutes.from_now } let(:iat) { Time.now } let :required_attributes do { iss: 'https://server.example.com', sub: 'user_id', aud: 'client_id', exp: ext, iat: iat } end describe 'attributes' do subject { klass } its(:required_attributes) { should == [:iss, :sub, :aud, :exp, :iat] } its(:optional_attributes) { should == [:acr, :amr, :azp, :jti, :sid, :auth_time, :nonce, :sub_jwk, :at_hash, :c_hash, :s_hash] } describe 'auth_time' do subject { id_token.auth_time } context 'when Time object given' do let(:attributes) do required_attributes.merge(auth_time: Time.now) end it do should be_a Numeric end end end end describe '#verify!' do context 'when both issuer, client_id are valid' do it do id_token.verify!( issuer: attributes[:iss], client_id: attributes[:aud] ).should == true end context 'when aud(ience) is an array of identifiers' do let(:client_id) { 'client_id' } let(:attributes) { required_attributes.merge(aud: ['some_other_identifier', client_id]) } it do id_token.verify!( issuer: attributes[:iss], client_id: client_id ).should == true end end context 'when expired' do let(:ext) { 10.minutes.ago } it do expect do id_token.verify!( issuer: attributes[:iss], client_id: attributes[:aud] ) end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken end end end context 'when issuer is invalid' do it do expect do id_token.verify!( issuer: 'invalid_issuer', client_id: attributes[:aud] ) end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken end end context 'when issuer is missing' do it do expect do id_token.verify!( client_id: attributes[:aud] ) end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken end end context 'when client_id is invalid' do it do expect do id_token.verify!( issuer: attributes[:iss], client_id: 'invalid_client' ) end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken end end context 'when client_id is missing' do it do expect do id_token.verify!( issuer: attributes[:iss] ) end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken end end context 'when nonce is given' do let(:attributes) { required_attributes.merge(nonce: 'nonce') } context 'when nonce is valid' do it do id_token.verify!( issuer: attributes[:iss], client_id: attributes[:aud], nonce: attributes[:nonce] ).should == true end end context 'when nonce is invalid' do it do expect do id_token.verify!( issuer: attributes[:iss], client_id: attributes[:aud], nonce: 'invalid_nonce' ) end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken end end context 'when nonce is missing' do it do expect do id_token.verify!( issuer: attributes[:iss], client_id: attributes[:aud] ) end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken end end end end describe '#to_jwt' do subject { id_token.to_jwt private_key } it { should be_a String } context 'when block given' do it 'should allow add additional headers' do t = id_token.to_jwt private_key do |t| t.header[:x5u] = "http://server.example.com/x5u" end h = Base64.urlsafe_decode64 t.split('.').first h.should include 'x5u' end end context 'when access_token is given' do shared_examples_for :id_token_with_at_hash do it 'should include at_hash' do t = id_token.to_jwt private_key jwt = JSON::JWT.decode t, public_key jwt.should include :at_hash jwt.should_not include :c_hash jwt[:at_hash].should == Base64.urlsafe_encode64( OpenSSL::Digest::SHA256.digest('access_token')[0, 128 / 8], padding: false ) end end context 'when access_token is a Rack::OAuth2::AccessToken' do before { id_token.access_token = Rack::OAuth2::AccessToken::Bearer.new(access_token: 'access_token') } it_should_behave_like :id_token_with_at_hash end context 'when access_token is a String' do before { id_token.access_token = 'access_token' } it_should_behave_like :id_token_with_at_hash end end context 'when code is given' do before { id_token.code = 'authorization_code' } it 'should include at_hash' do t = id_token.to_jwt private_key jwt = JSON::JWT.decode t, public_key jwt.should_not include :at_hash jwt.should include :c_hash jwt[:c_hash].should == Base64.urlsafe_encode64( OpenSSL::Digest::SHA256.digest('authorization_code')[0, 128 / 8], padding: false ) end end context 'when both access_token and code are given' do before do id_token.access_token = 'access_token' id_token.code = 'authorization_code' end it 'should include at_hash' do t = id_token.to_jwt private_key jwt = JSON::JWT.decode t, public_key jwt.should include :at_hash jwt.should include :c_hash jwt[:at_hash].should == Base64.urlsafe_encode64( OpenSSL::Digest::SHA256.digest('access_token')[0, 128 / 8], padding: false ) jwt[:c_hash].should == Base64.urlsafe_encode64( OpenSSL::Digest::SHA256.digest('authorization_code')[0, 128 / 8], padding: false ) end end context 'when neither access_token nor code are given' do it 'should include at_hash' do t = id_token.to_jwt private_key jwt = JSON::JWT.decode t, public_key jwt.should_not include :at_hash, :c_hash end end end describe '#as_json' do subject { id_token.as_json } let(:attributes) { required_attributes } it do hash = required_attributes hash[:exp] = required_attributes[:exp].to_i should == hash end end describe '.decode' do subject { klass.decode id_token.to_jwt(private_key), public_key } let(:attributes) { required_attributes } it { should be_a klass } [:iss, :sub, :aud].each do |key| its(key) { should == attributes[key] } end its(:exp) { should == attributes[:exp].to_i } its(:raw_attributes) { should be_instance_of JSON::JWS } context 'when IdP config is given' do subject { klass.decode id_token.to_jwt(private_key), idp_config } let(:jwks) do jwk_str = File.read(File.join(__dir__, '../../mock_response/public_keys/jwks_with_private_key.json')) jwk = JSON::JWK::Set.new JSON.parse(jwk_str) end let(:idp_config) do OpenIDConnect::Discovery::Provider::Config::Response.new( issuer: attributes[:issuer], authorization_endpoint: File.join(attributes[:iss], 'authorize'), jwks_uri: File.join(attributes[:iss], 'jwks'), response_types_supported: ['code'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: ['RS256'] ) end context 'when id_token has kid' do let(:private_key) do OpenSSL::PKey::RSA.new( File.read(File.join(__dir__, '../../mock_response/public_keys/private_key.pem')) ).to_jwk end it do mock_json :get, idp_config.jwks_uri, 'public_keys/jwks_with_private_key' do should be_a klass end end end context 'otherwise' do let(:private_key) do OpenSSL::PKey::RSA.new( File.read(File.join(__dir__, '../../mock_response/public_keys/private_key.pem')) ) end it do mock_json :get, idp_config.jwks_uri, 'public_keys/jwks_with_private_key' do expect do should end.to raise_error JSON::JWK::Set::KidNotFound end end end end context 'when self-issued' do context 'when valid' do let(:self_issued) do 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NlbGYtaXNzdWVkLm1lIiwic3ViIjoiMmdDUWFLUmJkY0RaeUlDTE92ODJJR2EtdHBSVU52QW1ZN3BnZ3Z5NGdENCIsImF1ZCI6ImNsaWVudC5leGFtcGxlLmNvbSIsImV4cCI6MTQ0MjQ4Mjc4MiwiaWF0IjoxNDQxODc3OTgyLCJzdWJfandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwibiI6IjN1RzNiSTV6MTFhM1hlOXUyZFVJNDBpcWZrVl9vTmFQVmNlalN4V3l0YnMybTZKMGMzNjJESlJQNGtyUl9TZjNtQXJ3Qjd6Qm5UWExkbW1tZW85VzloSDhsSnFGOUthMTY3dHBTQWJCajB1MjhyaTgwZFZ4NUxzblJTX19uUUd6Y3dNa2sxTTBERUx2X0FXbVYwU2JudDhJZEpSeFhwdG5xRE5tWXJ0cmItMkk0a1lwRHlwN2pvTXd0bDNXeGp2cnkwbENLNExqOU9SeXdod05zYUU2MHFsako5aHBGZV8wTmpmaThzaVBlMDRJSkFaUjl3NXo0TnAtQS1HbWdmeTNJTmNZVFYyQ25FekNSY29HSGl5OGduRzA1a015TnRtZTFVdV8xanBhdF9lcF9QUG9PWEJ6Q1NwbzB5QlRNSWhmdEJTQ3p2a2V1ZFdhNks2aW5LMkYxdyJ9fQ.wchF80oFxdjEcOEwPZ9TUlV6R96Vz8XK9MzednMOsZmEMnNSEqKKTyO0Mhp9lijJPZX8J7lTtAGkz4gfsjyoYBIHQOTf0qHRHSx9RTeC31whw1TJ9x5V6UXpKN0EW1EhjAEGIZ0EyFJ-cRTgVs0V7PT7e63JOUYyW6LqqHa4MV9SdK8BdnaN0D4-402Pf7yFqjneSHq3KZbXcgjUPT_hszsGvnn9qEyuIHQqON6YnDt55z5SvP_RfKtBfUe2VY-yglJT41LfhkIgpvjLYdYYRPh9G9ftJr17qht5RtHSNpTp4FPw7BR7rCnptb4xTxyq-sLu7qjSLRtqQ35Xpi_6qQ' end context 'when key == :self_issued' do it do expect do klass.decode self_issued, :self_issued end.not_to raise_error end end context 'when key == public_key' do it do expect do klass.decode self_issued, public_key end.to raise_error JSON::JWS::VerificationFailed end end end context 'when invalid subject' do let(:self_issued) do 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NlbGYtaXNzdWVkLm1lIiwic3ViIjoiUFdFYXFfVnlUd1hTSFR4QVlSZHdWTjNMN2s0UnNxOVBwaTZ4WHZ6ZGZWTSIsImF1ZCI6InRhcGlkLnRhcGlkZW50aXR5LmNvbSIsImV4cCI6MTM2MjI3OTkwMCwiaWF0IjoxMzYyMjc2MzAwLCJzdWJfandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwibiI6IjRGTWl5M08zbFlOd2RzeC15aXVjemRsek81eU11d1p4WFlzSDgydmM0RkM0QXgyMGpNVV94emJHSUhWVUtFQ0pndFp3clBlajhRSWUtZFZFYXQtaGxjNTB5TXluM0h3cmtJVjVZOTdET1E2Sks4azk2QTFqVWxPLW5sRjl4ZUx2VDlwYTJXRTZtYm1KOG5EQW5mR0d6bmRNd3VKNzVLZDI2YmZHY21wcm5qUUJLTkVrakdJbW9MMEhFODFUcjROeC1tN1lsYkRGaVFNRDVpYjhCY3N4S0tvMTZTeG5tSi1EeUY2c094Y2JtV1ZrdkZBa3FKWFBnVFVoNXVYT3YwYk9nN0I2d2RHdUMtWnpJUl8tdUx3YlcxN2V4NGx3ZTFPb0ppdFJ3SFczYlo3NEc3RkdoSmhfTUp4YzB3WXBkbW5uNVpjRFFOWl9sWVRvMHYzaU1PUWk3USJ9fQ.DZKaSne22DjKFSpSUphsTeCMkcMWDexQCm8BPb1nI1PzQYsEAOfwumDajt85UA0x28y2zuOevMj29VpwTzbpRDkduv2NWAI4MHw8DYEsIN__-QGANmdU1sKmthET2iFmeFySwWomLqFvYIaNmVYVLkD53Zqfct5qH3Wznd_hrK8T1d6Cxg-gyZlAeqEu2V8EL2yuz8Gdaeze4b78l5Ux-B_5FQhZ3UkXbL1B2gzKJQVKAQdFJb9zUfzmCeIiUmeM9mw_VU64tAvFDRiTKS1P6b62Gxuyx1DhMLFg2evDaTJERJOta9ywtPfdcLH3qcIiUBffP2-FnAz44bOlKzJorQ' end it do expect do klass.decode self_issued, :self_issued end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken, 'Invalid subject' end end context 'when no sub_jwk' do let(:self_issued) do 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkZXZpY2VfdG9rZW4iOiI2NjYxNmI2NTJkNjQ2NTc2Njk2MzY1MmQ3NDZmNmI2NTZlIiwiaXNzIjoiaHR0cHM6Ly9zZWxmLWlzc3VlZC5tZSIsInN1YiI6IlBXRWFxX1Z5VHdYU0hUeEFZUmR3Vk4zTDdrNFJzcTlQcGk2eFh2emRmVk0iLCJhdWQiOiJ0YXBpZC50YXBpZGVudGl0eS5jb20iLCJleHAiOjEzNjIyODAxNDQsImlhdCI6MTM2MjI3NjU0NH0.HtkguN4xOzJ-yh_kd2JCmG6fgDEiVY5VCgTWUD9l8YOgHjTT7LRZC3b1sNDgkdwBteX3eQIQOVxaYWp4-ftczaIlrznB0jxldqdEdB1Tr591YsiDcyOqmemo1ZYzOKhe_q1l68bdKKeHLc83BzlsJpS659uFDuixvF7G_HIJpCdwckX7x6H3KK73hCLzoYCOVgr_lkFRVVHHAJXzxiUuERLD7JIvg5jCbgmqxArP-jYBdbscHHx8i-UP3WYFBEORBM2rXJuJzGvk4sLhZ4NVGBWyr0DJlE-aWKTyeg-_-4kLPd3d68-k3nLJ82iCwcap-BU_5otSmXufN3_ffq_tTw' end it do expect do klass.decode self_issued, :self_issued end.to raise_error OpenIDConnect::ResponseObject::IdToken::InvalidToken, 'Missing sub_jwk' end end end end describe '.self_issued' do subject { self_issued } let(:sub_jwk) { JSON::JWK.new(public_key) } let(:self_issued) do klass.self_issued( public_key: public_key, aud: 'client.example.com', exp: 1.week.from_now, iat: Time.now ) end [:iss, :sub, :aud, :exp, :iat, :sub_jwk].each do |attribute| its(attribute) { should be_present } end its(:iss) { should == 'https://self-issued.me' } its(:sub_jwk) { should == sub_jwk} its(:subject) { should == sub_jwk.thumbprint } end end