update some op interfaces
This commit is contained in:
parent
45230569d3
commit
8790b54e0d
11 changed files with 348 additions and 400 deletions
|
@ -17,7 +17,7 @@ type Authorizer interface {
|
||||||
Decoder() utils.Decoder
|
Decoder() utils.Decoder
|
||||||
Encoder() utils.Encoder
|
Encoder() utils.Encoder
|
||||||
Signer() Signer
|
Signer() Signer
|
||||||
IDTokenVerifier() IDTokenHintVerifier
|
IDTokenHintVerifier() IDTokenHintVerifier
|
||||||
Crypto() Crypto
|
Crypto() Crypto
|
||||||
Issuer() string
|
Issuer() string
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,6 @@ type AuthorizeValidator interface {
|
||||||
ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, IDTokenHintVerifier) (string, error)
|
ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, IDTokenHintVerifier) (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
|
|
||||||
|
|
||||||
func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
|
func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
Authorize(w, r, authorizer)
|
Authorize(w, r, authorizer)
|
||||||
|
@ -59,7 +53,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
if validater, ok := authorizer.(AuthorizeValidator); 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.IDTokenHintVerifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
|
@ -204,14 +198,14 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author
|
||||||
func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) {
|
func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) {
|
||||||
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
|
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if authReq.GetResponseType() == oidc.ResponseTypeCode {
|
if authReq.GetResponseType() == oidc.ResponseTypeCode {
|
||||||
AuthResponseCode(w, r, authReq, authorizer)
|
AuthResponseCode(w, r, authReq, authorizer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
AuthResponseToken(w, r, authReq, authorizer, client)
|
AuthResponseToken(w, r, authReq, authorizer, client)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//AuthResponseCode creates the successful code authentication response
|
//AuthResponseCode creates the successful code authentication response
|
||||||
|
|
|
@ -15,6 +15,12 @@ const (
|
||||||
AccessTokenTypeJWT
|
AccessTokenTypeJWT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ApplicationType int
|
||||||
|
|
||||||
|
type AuthMethod string
|
||||||
|
|
||||||
|
type AccessTokenType int
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
GetID() string
|
GetID() string
|
||||||
RedirectURIs() []string
|
RedirectURIs() []string
|
||||||
|
@ -28,10 +34,6 @@ type Client interface {
|
||||||
DevMode() bool
|
DevMode() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsConfidentialType(c Client) bool {
|
|
||||||
return c.ApplicationType() == ApplicationTypeWeb
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
|
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
|
||||||
for _, t := range types {
|
for _, t := range types {
|
||||||
if t == responseType {
|
if t == responseType {
|
||||||
|
@ -41,8 +43,6 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApplicationType int
|
func IsConfidentialType(c Client) bool {
|
||||||
|
return c.ApplicationType() == ApplicationTypeWeb
|
||||||
type AuthMethod string
|
}
|
||||||
|
|
||||||
type AccessTokenType int
|
|
||||||
|
|
|
@ -1,361 +0,0 @@
|
||||||
package op
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/schema"
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
|
||||||
|
|
||||||
"github.com/caos/logging"
|
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
|
||||||
"github.com/caos/oidc/pkg/rp"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultAuthorizationEndpoint = "authorize"
|
|
||||||
defaulTokenEndpoint = "oauth/token"
|
|
||||||
defaultIntrospectEndpoint = "introspect"
|
|
||||||
defaultUserinfoEndpoint = "userinfo"
|
|
||||||
defaultEndSessionEndpoint = "end_session"
|
|
||||||
defaultKeysEndpoint = "keys"
|
|
||||||
|
|
||||||
AuthMethodBasic AuthMethod = "client_secret_basic"
|
|
||||||
AuthMethodPost = "client_secret_post"
|
|
||||||
AuthMethodNone = "none"
|
|
||||||
|
|
||||||
CodeMethodS256 = "S256"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
DefaultEndpoints = &endpoints{
|
|
||||||
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
|
|
||||||
Token: NewEndpoint(defaulTokenEndpoint),
|
|
||||||
Introspection: NewEndpoint(defaultIntrospectEndpoint),
|
|
||||||
Userinfo: NewEndpoint(defaultUserinfoEndpoint),
|
|
||||||
EndSession: NewEndpoint(defaultEndSessionEndpoint),
|
|
||||||
JwksURI: NewEndpoint(defaultKeysEndpoint),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type DefaultOP struct {
|
|
||||||
config *Config
|
|
||||||
endpoints *endpoints
|
|
||||||
storage Storage
|
|
||||||
signer Signer
|
|
||||||
verifier IDTokenHintVerifier
|
|
||||||
crypto Crypto
|
|
||||||
http http.Handler
|
|
||||||
decoder *schema.Decoder
|
|
||||||
encoder *schema.Encoder
|
|
||||||
interceptors []HttpInterceptor
|
|
||||||
retry func(int) (bool, int)
|
|
||||||
timer <-chan time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Issuer string
|
|
||||||
CryptoKey [32]byte
|
|
||||||
DefaultLogoutRedirectURI string
|
|
||||||
CodeMethodS256 bool
|
|
||||||
// ScopesSupported: oidc.SupportedScopes,
|
|
||||||
// ResponseTypesSupported: responseTypes,
|
|
||||||
// GrantTypesSupported: oidc.SupportedGrantTypes,
|
|
||||||
// ClaimsSupported: oidc.SupportedClaims,
|
|
||||||
// IdTokenSigningAlgValuesSupported: []string{keys.SigningAlgorithm},
|
|
||||||
// SubjectTypesSupported: []string{"public"},
|
|
||||||
// TokenEndpointAuthMethodsSupported:
|
|
||||||
}
|
|
||||||
|
|
||||||
type endpoints struct {
|
|
||||||
Authorization Endpoint
|
|
||||||
Token Endpoint
|
|
||||||
Introspection Endpoint
|
|
||||||
Userinfo Endpoint
|
|
||||||
EndSession Endpoint
|
|
||||||
CheckSessionIframe Endpoint
|
|
||||||
JwksURI Endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
type DefaultOPOpts func(o *DefaultOP) error
|
|
||||||
|
|
||||||
func WithCustomAuthEndpoint(endpoint Endpoint) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
if err := endpoint.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.endpoints.Authorization = endpoint
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithCustomTokenEndpoint(endpoint Endpoint) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
if err := endpoint.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.endpoints.Token = endpoint
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithCustomUserinfoEndpoint(endpoint Endpoint) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
if err := endpoint.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.endpoints.Userinfo = endpoint
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithCustomEndSessionEndpoint(endpoint Endpoint) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
if err := endpoint.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.endpoints.EndSession = endpoint
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithCustomKeysEndpoint(endpoint Endpoint) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
if err := endpoint.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.endpoints.JwksURI = endpoint
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithHttpInterceptors(interceptors ...HttpInterceptor) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
o.interceptors = append(o.interceptors, interceptors...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithRetry(max int, sleep time.Duration) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
o.retry = func(count int) (bool, int) {
|
|
||||||
count++
|
|
||||||
if count == max {
|
|
||||||
return false, count
|
|
||||||
}
|
|
||||||
time.Sleep(sleep)
|
|
||||||
return true, count
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithTimer(timer <-chan time.Time) DefaultOPOpts {
|
|
||||||
return func(o *DefaultOP) error {
|
|
||||||
o.timer = timer
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDefaultOP(ctx context.Context, config *Config, storage Storage, opOpts ...DefaultOPOpts) (OpenIDProvider, error) {
|
|
||||||
err := ValidateIssuer(config.Issuer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p := &DefaultOP{
|
|
||||||
config: config,
|
|
||||||
storage: storage,
|
|
||||||
endpoints: DefaultEndpoints,
|
|
||||||
timer: make(<-chan time.Time),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, optFunc := range opOpts {
|
|
||||||
if err := optFunc(p); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyCh := make(chan jose.SigningKey)
|
|
||||||
p.signer = NewDefaultSigner(ctx, storage, keyCh)
|
|
||||||
go p.ensureKey(ctx, storage, keyCh, p.timer)
|
|
||||||
|
|
||||||
p.verifier = NewIDTokenHintVerifier(config.Issuer, p)
|
|
||||||
|
|
||||||
p.http = CreateRouter(p, p.interceptors...)
|
|
||||||
|
|
||||||
p.decoder = schema.NewDecoder()
|
|
||||||
p.decoder.IgnoreUnknownKeys(true)
|
|
||||||
|
|
||||||
p.encoder = schema.NewEncoder()
|
|
||||||
|
|
||||||
p.crypto = NewAESCrypto(config.CryptoKey)
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) Issuer() string {
|
|
||||||
return p.config.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) AuthorizationEndpoint() Endpoint {
|
|
||||||
return p.endpoints.Authorization
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) TokenEndpoint() Endpoint {
|
|
||||||
return Endpoint(p.endpoints.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) UserinfoEndpoint() Endpoint {
|
|
||||||
return Endpoint(p.endpoints.Userinfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) EndSessionEndpoint() Endpoint {
|
|
||||||
return Endpoint(p.endpoints.EndSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) KeysEndpoint() Endpoint {
|
|
||||||
return Endpoint(p.endpoints.JwksURI)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) AuthMethodPostSupported() bool {
|
|
||||||
return true //TODO: config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) CodeMethodS256Supported() bool {
|
|
||||||
return p.config.CodeMethodS256
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HttpHandler() http.Handler {
|
|
||||||
return p.http
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HandleDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
||||||
Discover(w, CreateDiscoveryConfig(p, p.Signer()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
|
||||||
keyID := ""
|
|
||||||
for _, sig := range jws.Signatures {
|
|
||||||
keyID = sig.Header.KeyID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
keySet, err := p.Storage().GetKeySet(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("error fetching keys")
|
|
||||||
}
|
|
||||||
payload, err, ok := rp.CheckKey(keyID, keySet.Keys, jws)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("invalid kid")
|
|
||||||
}
|
|
||||||
return payload, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) Decoder() utils.Decoder {
|
|
||||||
return p.decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) Encoder() utils.Encoder {
|
|
||||||
return p.encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) Storage() Storage {
|
|
||||||
return p.storage
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) Signer() Signer {
|
|
||||||
return p.signer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) Crypto() Crypto {
|
|
||||||
return p.crypto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) ClientJWTVerifier() oidc.Verifier {
|
|
||||||
return p.verifier
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) Probes() []ProbesFn {
|
|
||||||
return []ProbesFn{
|
|
||||||
ReadySigner(p.Signer()),
|
|
||||||
ReadyStorage(p.Storage()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HandleKeys(w http.ResponseWriter, r *http.Request) {
|
|
||||||
Keys(w, r, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HandleAuthorize(w http.ResponseWriter, r *http.Request) {
|
|
||||||
Authorize(w, r, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HandleAuthorizeCallback(w http.ResponseWriter, r *http.Request) {
|
|
||||||
AuthorizeCallback(w, r, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HandleExchange(w http.ResponseWriter, r *http.Request) {
|
|
||||||
reqType := r.FormValue("grant_type")
|
|
||||||
if reqType == "" {
|
|
||||||
RequestError(w, r, ErrInvalidRequest("grant_type missing"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch reqType {
|
|
||||||
case string(oidc.GrantTypeCode):
|
|
||||||
CodeExchange(w, r, p)
|
|
||||||
return
|
|
||||||
case string(oidc.GrantTypeBearer):
|
|
||||||
JWTExchange(w, r, p)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
TokenExchange(w, r, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HandleUserinfo(w http.ResponseWriter, r *http.Request) {
|
|
||||||
Userinfo(w, r, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) HandleEndSession(w http.ResponseWriter, r *http.Request) {
|
|
||||||
EndSession(w, r, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) DefaultLogoutRedirectURI() string {
|
|
||||||
return p.config.DefaultLogoutRedirectURI
|
|
||||||
}
|
|
||||||
func (p *DefaultOP) IDTokenVerifier() IDTokenHintVerifier {
|
|
||||||
return p.verifier
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DefaultOP) ensureKey(ctx context.Context, storage Storage, keyCh chan<- jose.SigningKey, timer <-chan time.Time) {
|
|
||||||
count := 0
|
|
||||||
timer = time.After(0)
|
|
||||||
errCh := make(chan error)
|
|
||||||
go storage.GetSigningKey(ctx, keyCh, errCh, timer)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case err := <-errCh:
|
|
||||||
if err == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, ok := err.(StorageNotFoundError)
|
|
||||||
if ok {
|
|
||||||
err := storage.SaveNewKeyPair(ctx)
|
|
||||||
if err == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ok, count = p.retry(count)
|
|
||||||
if ok {
|
|
||||||
timer = time.After(0)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logging.Log("OP-n6ynVE").WithError(err).Panic("error in key signer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -38,20 +38,12 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
ScopeOpenID = "openid"
|
|
||||||
ScopeProfile = "profile"
|
|
||||||
ScopeEmail = "email"
|
|
||||||
ScopePhone = "phone"
|
|
||||||
ScopeAddress = "address"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DefaultSupportedScopes = []string{
|
var DefaultSupportedScopes = []string{
|
||||||
ScopeOpenID,
|
oidc.ScopeOpenID,
|
||||||
ScopeProfile,
|
oidc.ScopeProfile,
|
||||||
ScopeEmail,
|
oidc.ScopeEmail,
|
||||||
ScopePhone,
|
oidc.ScopePhone,
|
||||||
ScopeAddress,
|
oidc.ScopeAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Scopes(c Configuration) []string {
|
func Scopes(c Configuration) []string {
|
||||||
|
|
|
@ -19,7 +19,9 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
||||||
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
||||||
keySet, err := k.Storage().GetKeySet(r.Context())
|
keySet, err := k.Storage().GetKeySet(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
utils.MarshalJSON(w, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
utils.MarshalJSON(w, keySet)
|
utils.MarshalJSON(w, keySet)
|
||||||
}
|
}
|
||||||
|
|
323
pkg/op/op.go
323
pkg/op/op.go
|
@ -1,18 +1,48 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/logging"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/schema"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/rp"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
healthzEndpoint = "/healthz"
|
healthzEndpoint = "/healthz"
|
||||||
readinessEndpoint = "/ready"
|
readinessEndpoint = "/ready"
|
||||||
|
defaultAuthorizationEndpoint = "authorize"
|
||||||
|
defaulTokenEndpoint = "oauth/token"
|
||||||
|
defaultIntrospectEndpoint = "introspect"
|
||||||
|
defaultUserinfoEndpoint = "userinfo"
|
||||||
|
defaultEndSessionEndpoint = "end_session"
|
||||||
|
defaultKeysEndpoint = "keys"
|
||||||
|
|
||||||
|
AuthMethodBasic AuthMethod = "client_secret_basic"
|
||||||
|
AuthMethodPost AuthMethod = "client_secret_post"
|
||||||
|
AuthMethodNone AuthMethod = "none"
|
||||||
|
|
||||||
|
CodeMethodS256 = "S256"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultEndpoints = &endpoints{
|
||||||
|
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
|
||||||
|
Token: NewEndpoint(defaulTokenEndpoint),
|
||||||
|
Introspection: NewEndpoint(defaultIntrospectEndpoint),
|
||||||
|
Userinfo: NewEndpoint(defaultUserinfoEndpoint),
|
||||||
|
EndSession: NewEndpoint(defaultEndSessionEndpoint),
|
||||||
|
JwksURI: NewEndpoint(defaultKeysEndpoint),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenIDProvider interface {
|
type OpenIDProvider interface {
|
||||||
|
@ -20,7 +50,7 @@ type OpenIDProvider interface {
|
||||||
Storage() Storage
|
Storage() Storage
|
||||||
Decoder() utils.Decoder
|
Decoder() utils.Decoder
|
||||||
Encoder() utils.Encoder
|
Encoder() utils.Encoder
|
||||||
IDTokenVerifier() IDTokenHintVerifier
|
IDTokenHintVerifier() IDTokenHintVerifier
|
||||||
Crypto() Crypto
|
Crypto() Crypto
|
||||||
DefaultLogoutRedirectURI() string
|
DefaultLogoutRedirectURI() string
|
||||||
Signer() Signer
|
Signer() Signer
|
||||||
|
@ -54,6 +84,293 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Issuer string
|
||||||
|
CryptoKey [32]byte
|
||||||
|
DefaultLogoutRedirectURI string
|
||||||
|
CodeMethodS256 bool
|
||||||
|
// ScopesSupported: oidc.SupportedScopes,
|
||||||
|
// ResponseTypesSupported: responseTypes,
|
||||||
|
// GrantTypesSupported: oidc.SupportedGrantTypes,
|
||||||
|
// ClaimsSupported: oidc.SupportedClaims,
|
||||||
|
// IdTokenSigningAlgValuesSupported: []string{keys.SigningAlgorithm},
|
||||||
|
// SubjectTypesSupported: []string{"public"},
|
||||||
|
// TokenEndpointAuthMethodsSupported:
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpoints struct {
|
||||||
|
Authorization Endpoint
|
||||||
|
Token Endpoint
|
||||||
|
Introspection Endpoint
|
||||||
|
Userinfo Endpoint
|
||||||
|
EndSession Endpoint
|
||||||
|
CheckSessionIframe Endpoint
|
||||||
|
JwksURI Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) {
|
||||||
|
err := ValidateIssuer(config.Issuer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &openidProvider{
|
||||||
|
config: config,
|
||||||
|
storage: storage,
|
||||||
|
endpoints: DefaultEndpoints,
|
||||||
|
timer: make(<-chan time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, optFunc := range opOpts {
|
||||||
|
if err := optFunc(o); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyCh := make(chan jose.SigningKey)
|
||||||
|
o.signer = NewDefaultSigner(ctx, storage, keyCh)
|
||||||
|
go EnsureKey(ctx, storage, keyCh, o.timer, o.retry)
|
||||||
|
|
||||||
|
o.idTokenHintVerifier = NewIDTokenHintVerifier(config.Issuer, o)
|
||||||
|
|
||||||
|
o.httpHandler = CreateRouter(o, o.interceptors...)
|
||||||
|
|
||||||
|
o.decoder = schema.NewDecoder()
|
||||||
|
o.decoder.IgnoreUnknownKeys(true)
|
||||||
|
|
||||||
|
o.encoder = schema.NewEncoder()
|
||||||
|
|
||||||
|
o.crypto = NewAESCrypto(config.CryptoKey)
|
||||||
|
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type openidProvider struct {
|
||||||
|
config *Config
|
||||||
|
endpoints *endpoints
|
||||||
|
storage Storage
|
||||||
|
signer Signer
|
||||||
|
idTokenHintVerifier IDTokenHintVerifier
|
||||||
|
crypto Crypto
|
||||||
|
httpHandler http.Handler
|
||||||
|
decoder *schema.Decoder
|
||||||
|
encoder *schema.Encoder
|
||||||
|
interceptors []HttpInterceptor
|
||||||
|
retry func(int) (bool, int)
|
||||||
|
timer <-chan time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) Issuer() string {
|
||||||
|
return o.config.Issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) AuthorizationEndpoint() Endpoint {
|
||||||
|
return o.endpoints.Authorization
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) TokenEndpoint() Endpoint {
|
||||||
|
return o.endpoints.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) UserinfoEndpoint() Endpoint {
|
||||||
|
return o.endpoints.Userinfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) EndSessionEndpoint() Endpoint {
|
||||||
|
return o.endpoints.EndSession
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) KeysEndpoint() Endpoint {
|
||||||
|
return o.endpoints.JwksURI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) AuthMethodPostSupported() bool {
|
||||||
|
return true //todo: config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) CodeMethodS256Supported() bool {
|
||||||
|
return o.config.CodeMethodS256
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) Storage() Storage {
|
||||||
|
return o.storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) Decoder() utils.Decoder {
|
||||||
|
return o.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) Encoder() utils.Encoder {
|
||||||
|
return o.encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier {
|
||||||
|
return o.idTokenHintVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) Crypto() Crypto {
|
||||||
|
return o.crypto
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) DefaultLogoutRedirectURI() string {
|
||||||
|
return o.config.DefaultLogoutRedirectURI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) Signer() Signer {
|
||||||
|
return o.signer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) Probes() []ProbesFn {
|
||||||
|
return []ProbesFn{
|
||||||
|
ReadySigner(o.Signer()),
|
||||||
|
ReadyStorage(o.Storage()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *openidProvider) HttpHandler() http.Handler {
|
||||||
|
return o.httpHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
//VerifySignature implements the oidc.KeySet interface
|
||||||
|
//providing an implementation for the keys stored in the OP Storage interface
|
||||||
|
func (o *openidProvider) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
||||||
|
keyID := ""
|
||||||
|
for _, sig := range jws.Signatures {
|
||||||
|
keyID = sig.Header.KeyID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
keySet, err := o.Storage().GetKeySet(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("error fetching keys")
|
||||||
|
}
|
||||||
|
payload, err, ok := rp.CheckKey(keyID, keySet.Keys, jws)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid kid")
|
||||||
|
}
|
||||||
|
return payload, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureKey(ctx context.Context, storage Storage, keyCh chan<- jose.SigningKey, timer <-chan time.Time, retry func(int) (bool, int)) {
|
||||||
|
count := 0
|
||||||
|
timer = time.After(0)
|
||||||
|
errCh := make(chan error)
|
||||||
|
go storage.GetSigningKey(ctx, keyCh, errCh, timer)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case err := <-errCh:
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, ok := err.(StorageNotFoundError)
|
||||||
|
if ok {
|
||||||
|
err := storage.SaveNewKeyPair(ctx)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok, count = retry(count)
|
||||||
|
if ok {
|
||||||
|
timer = time.After(0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logging.Log("OP-n6ynVE").WithError(err).Panic("error in key signer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(o *openidProvider) error
|
||||||
|
|
||||||
|
func WithCustomAuthEndpoint(endpoint Endpoint) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.endpoints.Authorization = endpoint
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCustomTokenEndpoint(endpoint Endpoint) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.endpoints.Token = endpoint
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.endpoints.Userinfo = endpoint
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCustomEndSessionEndpoint(endpoint Endpoint) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.endpoints.EndSession = endpoint
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCustomKeysEndpoint(endpoint Endpoint) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.endpoints.JwksURI = endpoint
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCustomEndpoints(auth, token, userInfo, endSession, keys Endpoint) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
o.endpoints.Authorization = auth
|
||||||
|
o.endpoints.Token = token
|
||||||
|
o.endpoints.Userinfo = userInfo
|
||||||
|
o.endpoints.EndSession = endSession
|
||||||
|
o.endpoints.JwksURI = keys
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHttpInterceptors(interceptors ...HttpInterceptor) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
o.interceptors = append(o.interceptors, interceptors...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRetry(max int, sleep time.Duration) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
o.retry = func(count int) (bool, int) {
|
||||||
|
count++
|
||||||
|
if count == max {
|
||||||
|
return false, count
|
||||||
|
}
|
||||||
|
time.Sleep(sleep)
|
||||||
|
return true, count
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTimer(timer <-chan time.Time) Option {
|
||||||
|
return func(o *openidProvider) error {
|
||||||
|
o.timer = timer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler {
|
func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler {
|
||||||
return func(handlerFunc http.HandlerFunc) http.Handler {
|
return func(handlerFunc http.HandlerFunc) http.Handler {
|
||||||
handler := handlerFuncToHandler(handlerFunc)
|
handler := handlerFuncToHandler(handlerFunc)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
type SessionEnder interface {
|
type SessionEnder interface {
|
||||||
Decoder() utils.Decoder
|
Decoder() utils.Decoder
|
||||||
Storage() Storage
|
Storage() Storage
|
||||||
IDTokenVerifier() IDTokenHintVerifier
|
IDTokenHintVerifier() IDTokenHintVerifier
|
||||||
DefaultLogoutRedirectURI() string
|
DefaultLogoutRedirectURI() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
|
||||||
if req.IdTokenHint == "" {
|
if req.IdTokenHint == "" {
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenVerifier())
|
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidRequest("id_token_hint invalid")
|
return nil, ErrInvalidRequest("id_token_hint invalid")
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali
|
||||||
exp := time.Now().UTC().Add(validity)
|
exp := time.Now().UTC().Add(validity)
|
||||||
userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes())
|
userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
claims := &oidc.IDTokenClaims{
|
claims := &oidc.IDTokenClaims{
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
|
|
|
@ -155,6 +155,7 @@ func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rp.oauthConfig.Endpoint = endpoints.Endpoint
|
rp.oauthConfig.Endpoint = endpoints.Endpoint
|
||||||
|
rp.endpoints = endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
if rp.errorHandler == nil {
|
if rp.errorHandler == nil {
|
||||||
|
|
|
@ -109,6 +109,9 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet) IDTokenVeri
|
||||||
clientID: clientID,
|
clientID: clientID,
|
||||||
keySet: keySet,
|
keySet: keySet,
|
||||||
offset: 5 * time.Second,
|
offset: 5 * time.Second,
|
||||||
|
nonce: func(_ context.Context) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue