Merge branch 'master' into service-accounts

# Conflicts:
#	pkg/oidc/authorization.go
This commit is contained in:
Livio Amstutz 2020-09-07 12:36:10 +02:00
commit 6a0dd7c270
24 changed files with 395 additions and 142 deletions

8
go.mod
View file

@ -7,10 +7,10 @@ require (
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.4.1 // indirect github.com/google/go-cmp v0.4.1 // indirect
github.com/google/go-github/v31 v31.0.0 github.com/google/go-github/v31 v31.0.0
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.2
github.com/gorilla/handlers v1.4.2 github.com/gorilla/handlers v1.5.0
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.1.0 github.com/gorilla/schema v1.2.0
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/sirupsen/logrus v1.6.0 github.com/sirupsen/logrus v1.6.0

29
go.sum
View file

@ -4,8 +4,8 @@ github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -18,14 +18,14 @@ github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6C
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= github.com/gorilla/handlers v1.5.0 h1:4wjo3sf9azi99c8hTmyaxp9y5S+pFszsy3pP0rAw/lw=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.0/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -48,8 +48,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
@ -74,7 +72,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -94,13 +91,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -9,10 +9,40 @@ import (
) )
const ( const (
//ScopeOpenID defines the scope `openid`
//OpenID Connect requests MUST contain the `openid` scope value
ScopeOpenID = "openid" ScopeOpenID = "openid"
ResponseTypeCode ResponseType = "code" //ScopeProfile defines the scope `profile`
ResponseTypeIDToken ResponseType = "id_token token" //This (optional) scope value requests access to the End-User's default profile Claims,
//which are: name, family_name, given_name, middle_name, nickname, preferred_username,
//profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
ScopeProfile = "profile"
//ScopeEmail defines the scope `email`
//This (optional) scope value requests access to the email and email_verified Claims.
ScopeEmail = "email"
//ScopeAddress defines the scope `address`
//This (optional) scope value requests access to the address Claim.
ScopeAddress = "address"
//ScopePhone defines the scope `phone`
//This (optional) scope value requests access to the phone_number and phone_number_verified Claims.
ScopePhone = "phone"
//ScopeOfflineAccess defines the scope `offline_access`
//This (optional) scope value requests that an OAuth 2.0 Refresh Token be issued that can be used to obtain an Access Token
//that grants access to the End-User's UserInfo Endpoint even when the End-User is not present (not logged in).
ScopeOfflineAccess = "offline_access"
//ResponseTypeCode for the Authorization Code Flow returning a code from the Authorization Server
ResponseTypeCode ResponseType = "code"
//ResponseTypeIDToken for the Implicit Flow returning id and access tokens directly from the Authorization Server
ResponseTypeIDToken ResponseType = "id_token token"
//ResponseTypeIDTokenOnly for the Implicit Flow returning only id token directly from the Authorization Server
ResponseTypeIDTokenOnly ResponseType = "id_token" ResponseTypeIDTokenOnly ResponseType = "id_token"
DisplayPage Display = "page" DisplayPage Display = "page"
@ -20,14 +50,25 @@ const (
DisplayTouch Display = "touch" DisplayTouch Display = "touch"
DisplayWAP Display = "wap" DisplayWAP Display = "wap"
PromptNone Prompt = "none" //PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages.
PromptLogin Prompt = "login" //An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed
PromptConsent Prompt = "consent" PromptNone Prompt = "none"
//PromptLogin (`login`) directs the Authorization Server to prompt the End-User for reauthentication.
PromptLogin Prompt = "login"
//PromptConsent (`consent`) directs the Authorization Server to prompt the End-User for consent (of sharing information).
PromptConsent Prompt = "consent"
//PromptSelectAccount (`select_account `) directs the Authorization Server to prompt the End-User to select a user account (to enable multi user / session switching)
PromptSelectAccount Prompt = "select_account" PromptSelectAccount Prompt = "select_account"
//GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow
GrantTypeCode GrantType = "authorization_code" GrantTypeCode GrantType = "authorization_code"
//GrantTypeBearer define the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant
GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
//BearerToken defines the token_type `Bearer`, which is returned in a successful token response
BearerToken = "Bearer" BearerToken = "Bearer"
) )
@ -40,7 +81,6 @@ var displayValues = map[string]Display{
//AuthRequest according to: //AuthRequest according to:
//https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest //https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
//
type AuthRequest struct { type AuthRequest struct {
ID string ID string
Scopes Scopes `schema:"scope"` Scopes Scopes `schema:"scope"`
@ -65,12 +105,17 @@ type AuthRequest struct {
CodeChallengeMethod CodeChallengeMethod `schema:"code_challenge_method"` CodeChallengeMethod CodeChallengeMethod `schema:"code_challenge_method"`
} }
//GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface
func (a *AuthRequest) GetRedirectURI() string { func (a *AuthRequest) GetRedirectURI() string {
return a.RedirectURI return a.RedirectURI
} }
//GetResponseType returns the response_type value for the ErrAuthRequest interface
func (a *AuthRequest) GetResponseType() ResponseType { func (a *AuthRequest) GetResponseType() ResponseType {
return a.ResponseType return a.ResponseType
} }
//GetState returns the optional state value for the ErrAuthRequest interface
func (a *AuthRequest) GetState() string { func (a *AuthRequest) GetState() string {
return a.State return a.State
} }

View file

@ -5,10 +5,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/caos/oidc/pkg/utils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/caos/oidc/pkg/utils"
) )
type Tokens struct { type Tokens struct {
@ -61,7 +62,7 @@ type IDTokenClaims struct {
type jsonToken struct { type jsonToken struct {
Issuer string `json:"iss,omitempty"` Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"` Subject string `json:"sub,omitempty"`
Audiences []string `json:"aud,omitempty"` Audiences interface{} `json:"aud,omitempty"`
Expiration int64 `json:"exp,omitempty"` Expiration int64 `json:"exp,omitempty"`
NotBefore int64 `json:"nbf,omitempty"` NotBefore int64 `json:"nbf,omitempty"`
IssuedAt int64 `json:"iat,omitempty"` IssuedAt int64 `json:"iat,omitempty"`
@ -110,13 +111,9 @@ func (t *AccessTokenClaims) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &j); err != nil { if err := json.Unmarshal(b, &j); err != nil {
return err return err
} }
audience := j.Audiences
if len(audience) == 1 {
audience = strings.Split(audience[0], " ")
}
t.Issuer = j.Issuer t.Issuer = j.Issuer
t.Subject = j.Subject t.Subject = j.Subject
t.Audiences = audience t.Audiences = audienceFromJSON(j.Audiences)
t.Expiration = time.Unix(j.Expiration, 0).UTC() t.Expiration = time.Unix(j.Expiration, 0).UTC()
t.NotBefore = time.Unix(j.NotBefore, 0).UTC() t.NotBefore = time.Unix(j.NotBefore, 0).UTC()
t.IssuedAt = time.Unix(j.IssuedAt, 0).UTC() t.IssuedAt = time.Unix(j.IssuedAt, 0).UTC()
@ -161,13 +158,9 @@ func (t *IDTokenClaims) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &i); err != nil { if err := json.Unmarshal(b, &i); err != nil {
return err return err
} }
audience := i.Audiences
if len(audience) == 1 {
audience = strings.Split(audience[0], " ")
}
t.Issuer = i.Issuer t.Issuer = i.Issuer
t.Subject = i.Subject t.Subject = i.Subject
t.Audiences = audience t.Audiences = audienceFromJSON(i.Audiences)
t.Expiration = time.Unix(i.Expiration, 0).UTC() t.Expiration = time.Unix(i.Expiration, 0).UTC()
t.IssuedAt = time.Unix(i.IssuedAt, 0).UTC() t.IssuedAt = time.Unix(i.IssuedAt, 0).UTC()
t.AuthTime = time.Unix(i.AuthTime, 0).UTC() t.AuthTime = time.Unix(i.AuthTime, 0).UTC()
@ -247,3 +240,13 @@ func timeToJSON(t time.Time) int64 {
} }
return t.Unix() return t.Unix()
} }
func audienceFromJSON(audience interface{}) []string {
switch aud := audience.(type) {
case []string:
return aud
case string:
return []string{aud}
}
return nil
}

View file

@ -2,13 +2,11 @@ package op
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/rp"
@ -17,33 +15,37 @@ import (
type Authorizer interface { type Authorizer interface {
Storage() Storage Storage() Storage
Decoder() *schema.Decoder Decoder() utils.Decoder
Encoder() *schema.Encoder Encoder() utils.Encoder
Signer() Signer Signer() Signer
IDTokenVerifier() rp.Verifier IDTokenVerifier() rp.Verifier
Crypto() Crypto Crypto() Crypto
Issuer() string Issuer() string
} }
type ValidationAuthorizer interface { //AuthorizeValidator is an extension of Authorizer interface
//implementing it's own validation mechanism for the auth request
type AuthorizeValidator interface {
Authorizer Authorizer
ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, rp.Verifier) (string, error) ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, rp.Verifier) (string, error)
} }
//ValidationAuthorizer is an extension of Authorizer interface
//implementing it's own validation mechanism for the auth request
//
//Deprecated: ValidationAuthorizer exists for historical compatibility. Use ValidationAuthorizer itself
type ValidationAuthorizer AuthorizeValidator
//Authorize handles the authorization request, including
//parsing, validating, storing and finally redirecting to the login handler
func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
err := r.ParseForm() authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder())
if err != nil { if err != nil {
AuthRequestError(w, r, nil, ErrInvalidRequest("cannot parse form"), authorizer.Encoder()) AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
}
authReq := new(oidc.AuthRequest)
err = authorizer.Decoder().Decode(authReq, r.Form)
if err != nil {
AuthRequestError(w, r, nil, ErrInvalidRequest(fmt.Sprintf("cannot parse auth request: %v", err)), authorizer.Encoder())
return return
} }
validation := ValidateAuthRequest validation := ValidateAuthRequest
if validater, ok := authorizer.(ValidationAuthorizer); ok { if validater, ok := authorizer.(AuthorizeValidator); ok {
validation = validater.ValidateAuthRequest validation = validater.ValidateAuthRequest
} }
userID, err := validation(r.Context(), authReq, authorizer.Storage(), authorizer.IDTokenVerifier()) userID, err := validation(r.Context(), authReq, authorizer.Storage(), authorizer.IDTokenVerifier())
@ -64,6 +66,19 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
RedirectToLogin(req.GetID(), client, w, r) RedirectToLogin(req.GetID(), client, w, r)
} }
func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRequest, error) {
err := r.ParseForm()
if err != nil {
return nil, ErrInvalidRequest("cannot parse form")
}
authReq := new(oidc.AuthRequest)
err = decoder.Decode(authReq, r.Form)
if err != nil {
return nil, ErrInvalidRequest(fmt.Sprintf("cannot parse auth request: %v", err))
}
return authReq, nil
}
func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier rp.Verifier) (string, error) { func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier rp.Verifier) (string, error) {
client, err := storage.GetClientByClientID(ctx, authReq.ClientID) client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
if err != nil { if err != nil {
@ -95,7 +110,6 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res
if uri == "" { if uri == "" {
return ErrInvalidRequestRedirectURI("The redirect_uri is missing in the request. Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") return ErrInvalidRequestRedirectURI("The redirect_uri is missing in the request. Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.")
} }
if !utils.Contains(client.RedirectURIs(), uri) { if !utils.Contains(client.RedirectURIs(), uri) {
return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.")
} }
@ -138,7 +152,7 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie
if idTokenHint == "" { if idTokenHint == "" {
return "", nil return "", nil
} }
claims, err := verifier.Verify(ctx, "", idTokenHint) claims, err := verifier.VerifyIDToken(ctx, idTokenHint)
if err != nil { if err != nil {
return "", ErrInvalidRequest("The id_token_hint is invalid. If you have any questions, you may contact the administrator of the application.") return "", ErrInvalidRequest("The id_token_hint is invalid. If you have any questions, you may contact the administrator of the application.")
} }
@ -160,7 +174,7 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author
return return
} }
if !authReq.Done() { if !authReq.Done() {
AuthRequestError(w, r, authReq, errors.New("user not logged in"), authorizer.Encoder()) AuthRequestError(w, r, authReq, ErrInteractionRequired("Unfortunately, the user may is not logged in and/or additional interaction is required."), authorizer.Encoder())
return return
} }
AuthResponse(authReq, authorizer, w, r) AuthResponse(authReq, authorizer, w, r)

View file

@ -3,66 +3,140 @@ package op_test
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "net/url"
"reflect"
"testing" "testing"
"github.com/gorilla/schema"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/op" "github.com/caos/oidc/pkg/op"
"github.com/caos/oidc/pkg/op/mock" "github.com/caos/oidc/pkg/op/mock"
"github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
) )
func TestAuthorize(t *testing.T) { //
// testCallback := func(t *testing.T, clienID string) callbackHandler { //TOOD: tests will be implemented in branch for service accounts
// return func(authReq *oidc.AuthRequest, client oidc.Client, w http.ResponseWriter, r *http.Request) { //func TestAuthorize(t *testing.T) {
// // require.Equal(t, clientID, client.) // // testCallback := func(t *testing.T, clienID string) callbackHandler {
// } // // return func(authReq *oidc.AuthRequest, client oidc.Client, w http.ResponseWriter, r *http.Request) {
// } // // // require.Equal(t, clientID, client.)
// testErr := func(t *testing.T, expected error) errorHandler { // // }
// return func(w http.ResponseWriter, r *http.Request, authReq *oidc.AuthRequest, err error) { // // }
// require.Equal(t, expected, err) // // testErr := func(t *testing.T, expected error) errorHandler {
// } // // return func(w http.ResponseWriter, r *http.Request, authReq *oidc.AuthRequest, err error) {
// } // // require.Equal(t, expected, err)
// // }
// // }
// type args struct {
// w http.ResponseWriter
// r *http.Request
// authorizer op.Authorizer
// }
// tests := []struct {
// name string
// args args
// }{
// {
// "parsing fails",
// args{
// httptest.NewRecorder(),
// &http.Request{Method: "POST", Body: nil},
// mock.NewAuthorizerExpectValid(t, true),
// // testCallback(t, ""),
// // testErr(t, ErrInvalidRequest("cannot parse form")),
// },
// },
// {
// "decoding fails",
// args{
// httptest.NewRecorder(),
// func() *http.Request {
// r := httptest.NewRequest("POST", "/authorize", strings.NewReader("client_id=foo"))
// r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// return r
// }(),
// mock.NewAuthorizerExpectValid(t, true),
// // testCallback(t, ""),
// // testErr(t, ErrInvalidRequest("cannot parse auth request")),
// },
// },
// // {"decoding fails", args{httptest.NewRecorder(), &http.Request{}, mock.NewAuthorizerExpectValid(t), nil, testErr(t, nil)}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// op.Authorize(tt.args.w, tt.args.r, tt.args.authorizer)
// })
// }
//}
func TestParseAuthorizeRequest(t *testing.T) {
type args struct { type args struct {
w http.ResponseWriter r *http.Request
r *http.Request decoder utils.Decoder
authorizer op.Authorizer }
type res struct {
want *oidc.AuthRequest
err bool
} }
tests := []struct { tests := []struct {
name string name string
args args args args
res res
}{ }{
{ {
"parsing fails", "parsing form error",
args{ args{
httptest.NewRecorder(), &http.Request{URL: &url.URL{RawQuery: "invalid=%%param"}},
&http.Request{Method: "POST", Body: nil}, schema.NewDecoder(),
mock.NewAuthorizerExpectValid(t, true), },
// testCallback(t, ""), res{
// testErr(t, ErrInvalidRequest("cannot parse form")), nil,
true,
}, },
}, },
{ {
"decoding fails", "decoding error",
args{ args{
httptest.NewRecorder(), &http.Request{URL: &url.URL{RawQuery: "unknown=value"}},
func() *http.Request { func() utils.Decoder {
r := httptest.NewRequest("POST", "/authorize", strings.NewReader("client_id=foo")) decoder := schema.NewDecoder()
r.Header.Set("Content-Type", "application/x-www-form-urlencoded") decoder.IgnoreUnknownKeys(false)
return r return decoder
}(), }(),
mock.NewAuthorizerExpectValid(t, true), },
// testCallback(t, ""), res{
// testErr(t, ErrInvalidRequest("cannot parse auth request")), nil,
true,
},
},
{
"parsing ok",
args{
&http.Request{URL: &url.URL{RawQuery: "scope=openid"}},
func() utils.Decoder {
decoder := schema.NewDecoder()
decoder.IgnoreUnknownKeys(false)
return decoder
}(),
},
res{
&oidc.AuthRequest{Scopes: oidc.Scopes{"openid"}},
false,
}, },
}, },
// {"decoding fails", args{httptest.NewRecorder(), &http.Request{}, mock.NewAuthorizerExpectValid(t), nil, testErr(t, nil)}},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
op.Authorize(tt.args.w, tt.args.r, tt.args.authorizer) got, err := op.ParseAuthorizeRequest(tt.args.r, tt.args.decoder)
if (err != nil) != tt.res.err {
t.Errorf("ParseAuthorizeRequest() error = %v, wantErr %v", err, tt.res.err)
}
if !reflect.DeepEqual(got, tt.res.want) {
t.Errorf("ParseAuthorizeRequest() got = %v, want %v", got, tt.res.want)
}
}) })
} }
} }

View file

@ -1,8 +1,9 @@
package op package op
import ( import (
"github.com/caos/oidc/pkg/oidc"
"time" "time"
"github.com/caos/oidc/pkg/oidc"
) )
const ( const (

View file

@ -1,8 +1,9 @@
package op package op
import "testing" import (
"os"
import "os" "testing"
)
func TestValidateIssuer(t *testing.T) { func TestValidateIssuer(t *testing.T) {
type args struct { type args struct {
@ -78,7 +79,7 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
"localhost with http ok", "localhost with http with dev ok",
args{"http://localhost:9999"}, args{"http://localhost:9999"},
false, false,
}, },

View file

@ -13,6 +13,7 @@ import (
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
) )
const ( const (
@ -254,11 +255,11 @@ func (p *DefaultOP) VerifySignature(ctx context.Context, jws *jose.JSONWebSignat
return payload, err return payload, err
} }
func (p *DefaultOP) Decoder() *schema.Decoder { func (p *DefaultOP) Decoder() utils.Decoder {
return p.decoder return p.decoder
} }
func (p *DefaultOP) Encoder() *schema.Encoder { func (p *DefaultOP) Encoder() utils.Encoder {
return p.encoder return p.encoder
} }

View file

@ -6,12 +6,13 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/op"
"github.com/caos/oidc/pkg/op/mock"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/op"
"github.com/caos/oidc/pkg/op/mock"
) )
func TestDiscover(t *testing.T) { func TestDiscover(t *testing.T) {
@ -147,7 +148,7 @@ func TestSupportedClaims(t *testing.T) {
} }
func Test_SigAlgorithms(t *testing.T) { func Test_SigAlgorithms(t *testing.T) {
m := mock.NewMockSigner(gomock.NewController((t))) m := mock.NewMockSigner(gomock.NewController(t))
type args struct { type args struct {
s op.Signer s op.Signer
} }
@ -199,7 +200,7 @@ func Test_SubjectTypes(t *testing.T) {
} }
func Test_AuthMethods(t *testing.T) { func Test_AuthMethods(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController((t))) m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct { type args struct {
c op.Configuration c op.Configuration
} }

View file

@ -4,15 +4,15 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/gorilla/schema"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
) )
const ( const (
InvalidRequest errorType = "invalid_request" InvalidRequest errorType = "invalid_request"
ServerError errorType = "server_error" InvalidRequestURI errorType = "invalid_request_uri"
InteractionRequired errorType = "interaction_required"
ServerError errorType = "server_error"
) )
var ( var (
@ -24,11 +24,17 @@ var (
} }
ErrInvalidRequestRedirectURI = func(description string) *OAuthError { ErrInvalidRequestRedirectURI = func(description string) *OAuthError {
return &OAuthError{ return &OAuthError{
ErrorType: InvalidRequest, ErrorType: InvalidRequestURI,
Description: description, Description: description,
redirectDisabled: true, redirectDisabled: true,
} }
} }
ErrInteractionRequired = func(description string) *OAuthError {
return &OAuthError{
ErrorType: InteractionRequired,
Description: description,
}
}
ErrServerError = func(description string) *OAuthError { ErrServerError = func(description string) *OAuthError {
return &OAuthError{ return &OAuthError{
ErrorType: ServerError, ErrorType: ServerError,
@ -45,7 +51,7 @@ type ErrAuthRequest interface {
GetState() string GetState() string
} }
func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthRequest, err error, encoder *schema.Encoder) { func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthRequest, err error, encoder utils.Encoder) {
if authReq == nil { if authReq == nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
@ -56,7 +62,7 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq
e.ErrorType = ServerError e.ErrorType = ServerError
e.Description = err.Error() e.Description = err.Error()
} }
e.state = authReq.GetState() e.State = authReq.GetState()
if authReq.GetRedirectURI() == "" || e.redirectDisabled { if authReq.GetRedirectURI() == "" || e.redirectDisabled {
http.Error(w, e.Description, http.StatusBadRequest) http.Error(w, e.Description, http.StatusBadRequest)
return return
@ -89,9 +95,9 @@ func RequestError(w http.ResponseWriter, r *http.Request, err error) {
type OAuthError struct { type OAuthError struct {
ErrorType errorType `json:"error" schema:"error"` ErrorType errorType `json:"error" schema:"error"`
Description string `json:"error_description" schema:"error_description"` Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
state string `json:"state" schema:"state"` State string `json:"state,omitempty" schema:"state,omitempty"`
redirectDisabled bool redirectDisabled bool `json:"-" schema:"-"`
} }
func (e *OAuthError) Error() string { func (e *OAuthError) Error() string {

View file

@ -7,8 +7,8 @@ package mock
import ( import (
op "github.com/caos/oidc/pkg/op" op "github.com/caos/oidc/pkg/op"
rp "github.com/caos/oidc/pkg/rp" rp "github.com/caos/oidc/pkg/rp"
utils "github.com/caos/oidc/pkg/utils"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
schema "github.com/gorilla/schema"
reflect "reflect" reflect "reflect"
) )
@ -50,10 +50,10 @@ func (mr *MockAuthorizerMockRecorder) Crypto() *gomock.Call {
} }
// Decoder mocks base method // Decoder mocks base method
func (m *MockAuthorizer) Decoder() *schema.Decoder { func (m *MockAuthorizer) Decoder() utils.Decoder {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Decoder") ret := m.ctrl.Call(m, "Decoder")
ret0, _ := ret[0].(*schema.Decoder) ret0, _ := ret[0].(utils.Decoder)
return ret0 return ret0
} }
@ -64,10 +64,10 @@ func (mr *MockAuthorizerMockRecorder) Decoder() *gomock.Call {
} }
// Encoder mocks base method // Encoder mocks base method
func (m *MockAuthorizer) Encoder() *schema.Encoder { func (m *MockAuthorizer) Encoder() utils.Encoder {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Encoder") ret := m.ctrl.Call(m, "Encoder")
ret0, _ := ret[0].(*schema.Encoder) ret0, _ := ret[0].(utils.Encoder)
return ret0 return ret0
} }

View file

@ -69,6 +69,9 @@ type Verifier struct{}
func (v *Verifier) Verify(ctx context.Context, accessToken, idToken string) (*oidc.IDTokenClaims, error) { func (v *Verifier) Verify(ctx context.Context, accessToken, idToken string) (*oidc.IDTokenClaims, error) {
return nil, nil return nil, nil
} }
func (v *Verifier) VerifyIDToken(ctx context.Context, idToken string) (*oidc.IDTokenClaims, error) {
return nil, nil
}
type Sig struct{} type Sig struct{}

View file

@ -1,12 +1,12 @@
package mock package mock
import ( import (
"github.com/caos/oidc/pkg/oidc"
"testing" "testing"
gomock "github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
op "github.com/caos/oidc/pkg/op" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/op"
) )
func NewClient(t *testing.T) op.Client { func NewClient(t *testing.T) op.Client {

View file

@ -6,11 +6,11 @@ import (
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/rp"
"github.com/gorilla/schema" "github.com/caos/oidc/pkg/utils"
) )
type SessionEnder interface { type SessionEnder interface {
Decoder() *schema.Decoder Decoder() utils.Decoder
Storage() Storage Storage() Storage
IDTokenVerifier() rp.Verifier IDTokenVerifier() rp.Verifier
DefaultLogoutRedirectURI() string DefaultLogoutRedirectURI() string
@ -39,7 +39,7 @@ func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) {
http.Redirect(w, r, session.RedirectURI, http.StatusFound) http.Redirect(w, r, session.RedirectURI, http.StatusFound)
} }
func ParseEndSessionRequest(r *http.Request, decoder *schema.Decoder) (*oidc.EndSessionRequest, error) { func ParseEndSessionRequest(r *http.Request, decoder utils.Decoder) (*oidc.EndSessionRequest, error) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return nil, ErrInvalidRequest("error parsing form") return nil, ErrInvalidRequest("error parsing form")
@ -57,7 +57,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
if req.IdTokenHint == "" { if req.IdTokenHint == "" {
return session, nil return session, nil
} }
claims, err := ender.IDTokenVerifier().Verify(ctx, "", req.IdTokenHint) claims, err := ender.IDTokenVerifier().VerifyIDToken(ctx, req.IdTokenHint)
if err != nil { if err != nil {
return nil, ErrInvalidRequest("id_token_hint invalid") return nil, ErrInvalidRequest("id_token_hint invalid")
} }

View file

@ -8,6 +8,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/caos/logging" "github.com/caos/logging"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
) )

View file

@ -6,8 +6,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/gorilla/schema"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
@ -16,7 +14,7 @@ import (
type Exchanger interface { type Exchanger interface {
Issuer() string Issuer() string
Storage() Storage Storage() Storage
Decoder() *schema.Decoder Decoder() utils.Decoder
Signer() Signer Signer() Signer
Crypto() Crypto Crypto() Crypto
AuthMethodPostSupported() bool AuthMethodPostSupported() bool
@ -49,7 +47,7 @@ func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
utils.MarshalJSON(w, resp) utils.MarshalJSON(w, resp)
} }
func ParseAccessTokenRequest(r *http.Request, decoder *schema.Decoder) (*oidc.AccessTokenRequest, error) { func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.AccessTokenRequest, error) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return nil, ErrInvalidRequest("error parsing form") return nil, ErrInvalidRequest("error parsing form")

View file

@ -5,14 +5,12 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/gorilla/schema"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
) )
type UserinfoProvider interface { type UserinfoProvider interface {
Decoder() *schema.Decoder Decoder() utils.Decoder
Crypto() Crypto Crypto() Crypto
Storage() Storage Storage() Storage
} }
@ -37,7 +35,7 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
utils.MarshalJSON(w, info) utils.MarshalJSON(w, info)
} }
func getAccessToken(r *http.Request, decoder *schema.Decoder) (string, error) { func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) {
authHeader := r.Header.Get("authorization") authHeader := r.Header.Get("authorization")
if authHeader != "" { if authHeader != "" {
parts := strings.Split(authHeader, "Bearer ") parts := strings.Split(authHeader, "Bearer ")

View file

@ -169,14 +169,8 @@ func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString
return claims, nil return claims, nil
} }
func (v *DefaultVerifier) now() time.Time { //Verify implements the `VerifyIDToken` method of the `Verifier` interface
if v.config.now.IsZero() { //according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
v.config.now = time.Now().UTC().Round(time.Second)
}
return v.config.now
}
//VerifyIDToken: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) { func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) {
//1. if encrypted --> decrypt //1. if encrypted --> decrypt
decrypted, err := v.decryptToken(idTokenString) decrypted, err := v.decryptToken(idTokenString)
@ -238,6 +232,13 @@ func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString strin
return claims, nil return claims, nil
} }
func (v *DefaultVerifier) now() time.Time {
if v.config.now.IsZero() {
v.config.now = time.Now().UTC().Round(time.Second)
}
return v.config.now
}
func (v *DefaultVerifier) parseToken(tokenString string) (*oidc.IDTokenClaims, []byte, error) { func (v *DefaultVerifier) parseToken(tokenString string) (*oidc.IDTokenClaims, []byte, error) {
parts := strings.Split(tokenString, ".") parts := strings.Split(tokenString, ".")
if len(parts) != 3 { if len(parts) != 3 {
@ -383,7 +384,7 @@ func (v *DefaultVerifier) decryptToken(tokenString string) (string, error) {
} }
func (v *DefaultVerifier) verifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { func (v *DefaultVerifier) verifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
if accessToken == "" { if atHash == "" {
return nil return nil
} }

3
pkg/rp/mock/generate.go Normal file
View file

@ -0,0 +1,3 @@
package mock
//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/caos/oidc/pkg/rp Verifier

View file

@ -0,0 +1,65 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/caos/oidc/pkg/rp (interfaces: Verifier)
// Package mock is a generated GoMock package.
package mock
import (
context "context"
oidc "github.com/caos/oidc/pkg/oidc"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockVerifier is a mock of Verifier interface
type MockVerifier struct {
ctrl *gomock.Controller
recorder *MockVerifierMockRecorder
}
// MockVerifierMockRecorder is the mock recorder for MockVerifier
type MockVerifierMockRecorder struct {
mock *MockVerifier
}
// NewMockVerifier creates a new mock instance
func NewMockVerifier(ctrl *gomock.Controller) *MockVerifier {
mock := &MockVerifier{ctrl: ctrl}
mock.recorder = &MockVerifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockVerifier) EXPECT() *MockVerifierMockRecorder {
return m.recorder
}
// Verify mocks base method
func (m *MockVerifier) Verify(arg0 context.Context, arg1, arg2 string) (*oidc.IDTokenClaims, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.IDTokenClaims)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Verify indicates an expected call of Verify
func (mr *MockVerifierMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockVerifier)(nil).Verify), arg0, arg1, arg2)
}
// VerifyIDToken mocks base method
func (m *MockVerifier) VerifyIDToken(arg0 context.Context, arg1 string) (*oidc.IDTokenClaims, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
ret0, _ := ret[0].(*oidc.IDTokenClaims)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifyIDToken indicates an expected call of VerifyIDToken
func (mr *MockVerifierMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockVerifier)(nil).VerifyIDToken), arg0, arg1)
}

View file

@ -0,0 +1,37 @@
package mock
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
)
func NewVerifier(t *testing.T) rp.Verifier {
return NewMockVerifier(gomock.NewController(t))
}
func NewMockVerifierExpectInvalid(t *testing.T) rp.Verifier {
m := NewVerifier(t)
ExpectVerifyInvalid(m)
return m
}
func ExpectVerifyInvalid(v rp.Verifier) {
mock := v.(*MockVerifier)
mock.EXPECT().VerifyIDToken(gomock.Any(), gomock.Any()).Return(nil, errors.New("invalid"))
}
func NewMockVerifierExpectValid(t *testing.T) rp.Verifier {
m := NewVerifier(t)
ExpectVerifyValid(m)
return m
}
func ExpectVerifyValid(v rp.Verifier) {
mock := v.(*MockVerifier)
mock.EXPECT().VerifyIDToken(gomock.Any(), gomock.Any()).Return(&oidc.IDTokenClaims{Userinfo: oidc.Userinfo{Subject: "id"}}, nil)
}

View file

@ -12,4 +12,7 @@ type Verifier interface {
//Verify checks the access_token and id_token and returns the `id token claims` //Verify checks the access_token and id_token and returns the `id token claims`
Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error)
//VerifyIDToken checks the id_token only and returns its `id token claims`
VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error)
} }

View file

@ -18,6 +18,13 @@ var (
} }
) )
type Decoder interface {
Decode(dst interface{}, src map[string][]string) error
}
type Encoder interface {
Encode(src interface{}, dst map[string][]string) error
}
func FormRequest(endpoint string, request interface{}) (*http.Request, error) { func FormRequest(endpoint string, request interface{}) (*http.Request, error) {
form := make(map[string][]string) form := make(map[string][]string)
encoder := schema.NewEncoder() encoder := schema.NewEncoder()
@ -56,7 +63,7 @@ func HttpRequest(client *http.Client, req *http.Request, response interface{}) e
return nil return nil
} }
func URLEncodeResponse(resp interface{}, encoder *schema.Encoder) (string, error) { func URLEncodeResponse(resp interface{}, encoder Encoder) (string, error) {
values := make(map[string][]string) values := make(map[string][]string)
err := encoder.Encode(resp, values) err := encoder.Encode(resp, values)
if err != nil { if err != nil {