update some op interfaces

This commit is contained in:
Livio Amstutz 2020-09-15 07:25:44 +02:00
parent 45230569d3
commit 8790b54e0d
11 changed files with 348 additions and 400 deletions

View file

@ -17,7 +17,7 @@ type Authorizer interface {
Decoder() utils.Decoder
Encoder() utils.Encoder
Signer() Signer
IDTokenVerifier() IDTokenHintVerifier
IDTokenHintVerifier() IDTokenHintVerifier
Crypto() Crypto
Issuer() string
}
@ -29,12 +29,6 @@ type AuthorizeValidator interface {
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) {
return func(w http.ResponseWriter, r *http.Request) {
Authorize(w, r, authorizer)
@ -59,7 +53,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
if validater, ok := authorizer.(AuthorizeValidator); ok {
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 {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
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) {
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
}
if authReq.GetResponseType() == oidc.ResponseTypeCode {
AuthResponseCode(w, r, authReq, authorizer)
return
}
AuthResponseToken(w, r, authReq, authorizer, client)
return
}
//AuthResponseCode creates the successful code authentication response

View file

@ -15,6 +15,12 @@ const (
AccessTokenTypeJWT
)
type ApplicationType int
type AuthMethod string
type AccessTokenType int
type Client interface {
GetID() string
RedirectURIs() []string
@ -28,10 +34,6 @@ type Client interface {
DevMode() bool
}
func IsConfidentialType(c Client) bool {
return c.ApplicationType() == ApplicationTypeWeb
}
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
for _, t := range types {
if t == responseType {
@ -41,8 +43,6 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT
return false
}
type ApplicationType int
type AuthMethod string
type AccessTokenType int
func IsConfidentialType(c Client) bool {
return c.ApplicationType() == ApplicationTypeWeb
}

View file

@ -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")
}
}
}

View file

@ -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{
ScopeOpenID,
ScopeProfile,
ScopeEmail,
ScopePhone,
ScopeAddress,
oidc.ScopeOpenID,
oidc.ScopeProfile,
oidc.ScopeEmail,
oidc.ScopePhone,
oidc.ScopeAddress,
}
func Scopes(c Configuration) []string {

View file

@ -19,7 +19,9 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
keySet, err := k.Storage().GetKeySet(r.Context())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
utils.MarshalJSON(w, err)
return
}
utils.MarshalJSON(w, keySet)
}

View file

@ -1,18 +1,48 @@
package op
import (
"context"
"errors"
"net/http"
"time"
"github.com/caos/logging"
"github.com/gorilla/handlers"
"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/rp"
"github.com/caos/oidc/pkg/utils"
)
const (
healthzEndpoint = "/healthz"
readinessEndpoint = "/ready"
healthzEndpoint = "/healthz"
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 {
@ -20,7 +50,7 @@ type OpenIDProvider interface {
Storage() Storage
Decoder() utils.Decoder
Encoder() utils.Encoder
IDTokenVerifier() IDTokenHintVerifier
IDTokenHintVerifier() IDTokenHintVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
Signer() Signer
@ -54,6 +84,293 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.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 {
return func(handlerFunc http.HandlerFunc) http.Handler {
handler := handlerFuncToHandler(handlerFunc)

View file

@ -11,7 +11,7 @@ import (
type SessionEnder interface {
Decoder() utils.Decoder
Storage() Storage
IDTokenVerifier() IDTokenHintVerifier
IDTokenHintVerifier() IDTokenHintVerifier
DefaultLogoutRedirectURI() string
}
@ -62,7 +62,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
if req.IdTokenHint == "" {
return session, nil
}
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenVerifier())
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier())
if err != nil {
return nil, ErrInvalidRequest("id_token_hint invalid")
}

View file

@ -102,7 +102,7 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali
exp := time.Now().UTC().Add(validity)
userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes())
if err != nil {
return "", err
}
claims := &oidc.IDTokenClaims{
Issuer: issuer,