File: client.go

package info (click to toggle)
golang-github-zitadel-oidc 3.37.0-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental, sid, trixie
  • size: 1,484 kB
  • sloc: makefile: 5
file content (201 lines) | stat: -rw-r--r-- 6,594 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package op

import (
	"context"
	"errors"
	"net/http"
	"net/url"
	"time"

	httphelper "github.com/zitadel/oidc/v3/pkg/http"
	"github.com/zitadel/oidc/v3/pkg/oidc"
)

//go:generate go get github.com/dmarkham/enumer
//go:generate go run github.com/dmarkham/enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType
//go:generate go mod tidy

const (
	ApplicationTypeWeb       ApplicationType = iota // web
	ApplicationTypeUserAgent                        // user_agent
	ApplicationTypeNative                           // native
)

const (
	AccessTokenTypeBearer AccessTokenType = iota // bearer
	AccessTokenTypeJWT                           // JWT
)

type ApplicationType int

type AuthMethod string

type AccessTokenType int

type Client interface {
	GetID() string
	RedirectURIs() []string
	PostLogoutRedirectURIs() []string
	ApplicationType() ApplicationType
	AuthMethod() oidc.AuthMethod
	ResponseTypes() []oidc.ResponseType
	GrantTypes() []oidc.GrantType
	LoginURL(string) string
	AccessTokenType() AccessTokenType
	IDTokenLifetime() time.Duration
	DevMode() bool
	RestrictAdditionalIdTokenScopes() func(scopes []string) []string
	RestrictAdditionalAccessTokenScopes() func(scopes []string) []string
	IsScopeAllowed(scope string) bool
	IDTokenUserinfoClaimsAssertion() bool
	ClockSkew() time.Duration
}

// HasRedirectGlobs is an optional interface that can be implemented by implementors of
// Client. See https://pkg.go.dev/path#Match for glob
// interpretation. Redirect URIs that match either the non-glob version or the
// glob version will be accepted. Glob URIs are only partially supported for native
// clients: "http://" is not allowed except for loopback or in dev mode.
//
// Note that globbing / wildcards are not permitted by the OIDC
// standard and implementing this interface can have security implications.
// It is advised to only return a client of this type in rare cases,
// such as DevMode for the client being enabled.
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type HasRedirectGlobs interface {
	Client
	RedirectURIGlobs() []string
	PostLogoutRedirectURIGlobs() []string
}

func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
	for _, t := range types {
		if t == responseType {
			return true
		}
	}
	return false
}

func IsConfidentialType(c Client) bool {
	return c.ApplicationType() == ApplicationTypeWeb
}

var (
	ErrInvalidAuthHeader   = errors.New("invalid basic auth header")
	ErrNoClientCredentials = errors.New("no client credentials provided")
	ErrMissingClientID     = errors.New("client_id missing from request")
)

type ClientJWTProfile interface {
	JWTProfileVerifier(context.Context) *JWTProfileVerifier
}

func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) {
	ctx, span := tracer.Start(ctx, "ClientJWTAuth")
	defer span.End()

	if ca.ClientAssertion == "" {
		return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
	}

	profile, err := VerifyJWTAssertion(ctx, ca.ClientAssertion, verifier.JWTProfileVerifier(ctx))
	if err != nil {
		return "", oidc.ErrUnauthorizedClient().WithParent(err).WithDescription("JWT assertion failed")
	}
	return profile.Issuer, nil
}

func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) {
	ctx, span := tracer.Start(r.Context(), "ClientBasicAuth")
	r = r.WithContext(ctx)
	defer span.End()

	clientID, clientSecret, ok := r.BasicAuth()
	if !ok {
		return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
	}
	clientID, err = url.QueryUnescape(clientID)
	if err != nil {
		return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
	}
	clientSecret, err = url.QueryUnescape(clientSecret)
	if err != nil {
		return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
	}
	if err := storage.AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
		return "", oidc.ErrUnauthorizedClient().WithParent(err)
	}
	return clientID, nil
}

type ClientProvider interface {
	Decoder() httphelper.Decoder
	Storage() Storage
}

type clientData struct {
	ClientID string `schema:"client_id"`
	oidc.ClientAssertionParams
}

// ClientIDFromRequest parses the request form and tries to obtain the client ID
// and reports if it is authenticated, using a JWT or static client secrets over
// http basic auth.
//
// If the Provider implements IntrospectorJWTProfile and "client_assertion" is
// present in the form data, JWT assertion will be verified and the
// client ID is taken from there.
// If any of them is absent, basic auth is attempted.
// In absence of basic auth data, the unauthenticated client id from the form
// data is returned.
//
// If no client id can be obtained by any method, oidc.ErrInvalidClient
// is returned with ErrMissingClientID wrapped in it.
func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, authenticated bool, err error) {
	err = r.ParseForm()
	if err != nil {
		return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
	}

	ctx, span := tracer.Start(r.Context(), "ClientIDFromRequest")
	r = r.WithContext(ctx)
	defer span.End()

	data := new(clientData)
	if err = p.Decoder().Decode(data, r.Form); err != nil {
		return "", false, err
	}

	JWTProfile, ok := p.(ClientJWTProfile)
	if ok && data.ClientAssertion != "" {
		// if JWTProfile is supported and client sent an assertion, check it and use it as response
		// regardless if it succeeded or failed
		clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile)
		return clientID, err == nil, err
	}
	// try basic auth
	clientID, err = ClientBasicAuth(r, p.Storage())
	// if that succeeded, use it
	if err == nil {
		return clientID, true, nil
	}
	// if the client did not send a Basic Auth Header, ignore the `ErrNoClientCredentials`
	// but return other errors immediately
	if !errors.Is(err, ErrNoClientCredentials) {
		return "", false, err
	}

	// if the client did not authenticate (public clients) it must at least send a client_id
	if data.ClientID == "" {
		return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID)
	}
	return data.ClientID, false, nil
}

type ClientCredentials struct {
	ClientID            string `schema:"client_id"`
	ClientSecret        string `schema:"client_secret"`    // Client secret from Basic auth or request body
	ClientAssertion     string `schema:"client_assertion"` // JWT
	ClientAssertionType string `schema:"client_assertion_type"`
}