Merge branch 'next' into main-next

prepare the merge of next into main by resolving merge conflicts.
This commit is contained in:
Tim Möhlmann 2023-03-15 16:26:32 +02:00
commit 0476b5946e
122 changed files with 8195 additions and 2858 deletions

View file

@ -12,9 +12,9 @@ import (
"github.com/gorilla/mux"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
str "github.com/zitadel/oidc/pkg/strings"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
str "github.com/zitadel/oidc/v2/pkg/strings"
)
type AuthRequest interface {
@ -39,10 +39,8 @@ type Authorizer interface {
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
Signer() Signer
IDTokenHintVerifier() IDTokenHintVerifier
IDTokenHintVerifier(context.Context) IDTokenHintVerifier
Crypto() Crypto
Issuer() string
RequestObjectSupported() bool
}
@ -73,8 +71,9 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
}
ctx := r.Context()
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
authReq, err = ParseRequestObject(r.Context(), authReq, authorizer.Storage(), authorizer.Issuer())
authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
@ -92,7 +91,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.IDTokenHintVerifier())
userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
@ -101,12 +100,12 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder())
return
}
req, err := authorizer.Storage().CreateAuthRequest(r.Context(), authReq, userID)
req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID)
if err != nil {
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder())
return
}
client, err := authorizer.Storage().GetClientByClientID(r.Context(), req.GetClientID())
client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
if err != nil {
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder())
return
@ -390,7 +389,7 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie
if idTokenHint == "" {
return "", nil
}
claims, err := VerifyIDTokenHint(ctx, idTokenHint, verifier)
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier)
if err != nil {
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
"If you have any questions, you may contact the administrator of the application.")

View file

@ -13,10 +13,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/pkg/op/mock"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/v2/pkg/op/mock"
)
//

View file

@ -1,13 +1,19 @@
package op
import (
"context"
"errors"
"net/http"
"net/url"
"time"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/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
@ -67,3 +73,95 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT
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) {
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) {
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)
}
data := new(clientData)
if err = p.Decoder().Decode(data, r.PostForm); err != nil {
return "", false, err
}
JWTProfile, ok := p.(ClientJWTProfile)
if ok {
clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile)
}
if !ok || errors.Is(err, ErrNoClientCredentials) {
clientID, err = ClientBasicAuth(r, p.Storage())
}
if err == nil {
return clientID, true, nil
}
if data.ClientID == "" {
return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID)
}
return data.ClientID, false, nil
}

253
pkg/op/client_test.go Normal file
View file

@ -0,0 +1,253 @@
package op_test
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/gorilla/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/v2/pkg/op/mock"
)
type testClientJWTProfile struct{}
func (testClientJWTProfile) JWTProfileVerifier(context.Context) op.JWTProfileVerifier { return nil }
func TestClientJWTAuth(t *testing.T) {
type args struct {
ctx context.Context
ca oidc.ClientAssertionParams
verifier op.ClientJWTProfile
}
tests := []struct {
name string
args args
wantClientID string
wantErr error
}{
{
name: "empty assertion",
args: args{
context.Background(),
oidc.ClientAssertionParams{},
testClientJWTProfile{},
},
wantErr: op.ErrNoClientCredentials,
},
{
name: "verification error",
args: args{
context.Background(),
oidc.ClientAssertionParams{
ClientAssertion: "foo",
},
testClientJWTProfile{},
},
wantErr: oidc.ErrParse,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotClientID, err := op.ClientJWTAuth(tt.args.ctx, tt.args.ca, tt.args.verifier)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantClientID, gotClientID)
})
}
}
func TestClientBasicAuth(t *testing.T) {
errWrong := errors.New("wrong secret")
type args struct {
username string
password string
}
tests := []struct {
name string
args *args
storage op.Storage
wantClientID string
wantErr error
}{
{
name: "no args",
wantErr: op.ErrNoClientCredentials,
},
{
name: "username unescape err",
args: &args{
username: "%",
password: "bar",
},
wantErr: op.ErrInvalidAuthHeader,
},
{
name: "password unescape err",
args: &args{
username: "foo",
password: "%",
},
wantErr: op.ErrInvalidAuthHeader,
},
{
name: "auth error",
args: &args{
username: "foo",
password: "wrong",
},
storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong)
return s
}(),
wantErr: errWrong,
},
{
name: "auth error",
args: &args{
username: "foo",
password: "bar",
},
storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
return s
}(),
wantClientID: "foo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/foo", nil)
if tt.args != nil {
r.SetBasicAuth(tt.args.username, tt.args.password)
}
gotClientID, err := op.ClientBasicAuth(r, tt.storage)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantClientID, gotClientID)
})
}
}
type errReader struct{}
func (errReader) Read([]byte) (int, error) {
return 0, io.ErrNoProgress
}
type testClientProvider struct {
storage op.Storage
}
func (testClientProvider) Decoder() httphelper.Decoder {
return schema.NewDecoder()
}
func (p testClientProvider) Storage() op.Storage {
return p.storage
}
func TestClientIDFromRequest(t *testing.T) {
type args struct {
body io.Reader
p op.ClientProvider
}
type basicAuth struct {
username string
password string
}
tests := []struct {
name string
args args
basicAuth *basicAuth
wantClientID string
wantAuthenticated bool
wantErr bool
}{
{
name: "parse error",
args: args{
body: errReader{},
},
wantErr: true,
},
{
name: "unauthenticated",
args: args{
body: strings.NewReader(
url.Values{
"client_id": []string{"foo"},
}.Encode(),
),
p: testClientProvider{
storage: mock.NewStorage(t),
},
},
wantClientID: "foo",
wantAuthenticated: false,
},
{
name: "authenticated",
args: args{
body: strings.NewReader(
url.Values{}.Encode(),
),
p: testClientProvider{
storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
return s
}(),
},
},
basicAuth: &basicAuth{
username: "foo",
password: "bar",
},
wantClientID: "foo",
wantAuthenticated: true,
},
{
name: "missing client id",
args: args{
body: strings.NewReader(
url.Values{}.Encode(),
),
p: testClientProvider{
storage: mock.NewStorage(t),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/foo", tt.args.body)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if tt.basicAuth != nil {
r.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password)
}
gotClientID, gotAuthenticated, err := op.ClientIDFromRequest(r, tt.args.p)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantClientID, gotClientID)
assert.Equal(t, tt.wantAuthenticated, gotAuthenticated)
})
}
}

View file

@ -2,20 +2,24 @@ package op
import (
"errors"
"net/http"
"net/url"
"os"
"strings"
"golang.org/x/text/language"
)
const (
OidcDevMode = "ZITADEL_OIDC_DEV"
// deprecated: use OidcDevMode (ZITADEL_OIDC_DEV=true)
devMode = "CAOS_OIDC_DEV"
var (
ErrInvalidIssuerPath = errors.New("no fragments or query allowed for issuer")
ErrInvalidIssuerNoIssuer = errors.New("missing issuer")
ErrInvalidIssuerURL = errors.New("invalid url for issuer")
ErrInvalidIssuerMissingHost = errors.New("host for issuer missing")
ErrInvalidIssuerHTTPS = errors.New("scheme for issuer must be `https`")
)
type Configuration interface {
Issuer() string
IssuerFromRequest(r *http.Request) string
Insecure() bool
AuthorizationEndpoint() Endpoint
TokenEndpoint() Endpoint
IntrospectionEndpoint() Endpoint
@ -23,6 +27,7 @@ type Configuration interface {
RevocationEndpoint() Endpoint
EndSessionEndpoint() Endpoint
KeysEndpoint() Endpoint
DeviceAuthorizationEndpoint() Endpoint
AuthMethodPostSupported() bool
CodeMethodS256Supported() bool
@ -32,6 +37,7 @@ type Configuration interface {
GrantTypeTokenExchangeSupported() bool
GrantTypeJWTAuthorizationSupported() bool
GrantTypeClientCredentialsSupported() bool
GrantTypeDeviceCodeSupported() bool
IntrospectionAuthMethodPrivateKeyJWTSupported() bool
IntrospectionEndpointSigningAlgorithmsSupported() []string
RevocationAuthMethodPrivateKeyJWTSupported() bool
@ -40,38 +46,77 @@ type Configuration interface {
RequestObjectSigningAlgorithmsSupported() []string
SupportedUILocales() []language.Tag
DeviceAuthorization() DeviceAuthorizationConfig
}
func ValidateIssuer(issuer string) error {
type IssuerFromRequest func(r *http.Request) string
func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) {
return func(allowInsecure bool) (IssuerFromRequest, error) {
issuerPath, err := url.Parse(path)
if err != nil {
return nil, ErrInvalidIssuerURL
}
if err := ValidateIssuerPath(issuerPath); err != nil {
return nil, err
}
return func(r *http.Request) string {
return dynamicIssuer(r.Host, path, allowInsecure)
}, nil
}
}
func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) {
return func(allowInsecure bool) (IssuerFromRequest, error) {
if err := ValidateIssuer(issuer, allowInsecure); err != nil {
return nil, err
}
return func(_ *http.Request) string {
return issuer
}, nil
}
}
func ValidateIssuer(issuer string, allowInsecure bool) error {
if issuer == "" {
return errors.New("missing issuer")
return ErrInvalidIssuerNoIssuer
}
u, err := url.Parse(issuer)
if err != nil {
return errors.New("invalid url for issuer")
return ErrInvalidIssuerURL
}
if u.Host == "" {
return errors.New("host for issuer missing")
return ErrInvalidIssuerMissingHost
}
if u.Scheme != "https" {
if !devLocalAllowed(u) {
return errors.New("scheme for issuer must be `https`")
if !devLocalAllowed(u, allowInsecure) {
return ErrInvalidIssuerHTTPS
}
}
if u.Fragment != "" || len(u.Query()) > 0 {
return errors.New("no fragments or query allowed for issuer")
return ValidateIssuerPath(u)
}
func ValidateIssuerPath(issuer *url.URL) error {
if issuer.Fragment != "" || len(issuer.Query()) > 0 {
return ErrInvalidIssuerPath
}
return nil
}
func devLocalAllowed(url *url.URL) bool {
_, b := os.LookupEnv(OidcDevMode)
if !b {
// check the old / current env var as well
_, b = os.LookupEnv(devMode)
if !b {
return b
}
func devLocalAllowed(url *url.URL, allowInsecure bool) bool {
if !allowInsecure {
return false
}
return url.Scheme == "http"
}
func dynamicIssuer(issuer, path string, allowInsecure bool) string {
schema := "https"
if allowInsecure {
schema = "http"
}
if len(path) > 0 && !strings.HasPrefix(path, "/") {
path = "/" + path
}
return schema + "://" + issuer + path
}

View file

@ -1,13 +1,17 @@
package op
import (
"os"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateIssuer(t *testing.T) {
type args struct {
issuer string
issuer string
allowInsecure bool
}
tests := []struct {
name string
@ -16,65 +20,97 @@ func TestValidateIssuer(t *testing.T) {
}{
{
"missing issuer fails",
args{""},
args{
issuer: "",
},
true,
},
{
"invalid url for issuer fails",
args{":issuer"},
true,
},
{
"invalid url for issuer fails",
args{":issuer"},
args{
issuer: ":issuer",
},
true,
},
{
"host for issuer missing fails",
args{"https:///issuer"},
true,
},
{
"host for not https fails",
args{"http://issuer.com"},
args{
issuer: "https:///issuer",
},
true,
},
{
"host with fragment fails",
args{"https://issuer.com/#issuer"},
args{
issuer: "https://issuer.com/#issuer",
},
true,
},
{
"host with query fails",
args{"https://issuer.com?issuer=me"},
args{
issuer: "https://issuer.com?issuer=me",
},
true,
},
{
"host with http fails",
args{
issuer: "http://issuer.com",
},
true,
},
{
"host with https ok",
args{"https://issuer.com"},
args{
issuer: "https://issuer.com",
},
false,
},
{
"localhost with http fails",
args{"http://localhost:9999"},
"custom scheme fails",
args{
issuer: "custom://localhost:9999",
},
true,
},
{
"http with allowInsecure ok",
args{
issuer: "http://localhost:9999",
allowInsecure: true,
},
false,
},
{
"https with allowInsecure ok",
args{
issuer: "https://localhost:9999",
allowInsecure: true,
},
false,
},
{
"custom scheme with allowInsecure fails",
args{
issuer: "custom://localhost:9999",
allowInsecure: true,
},
true,
},
}
// ensure env is not set
//nolint:errcheck
os.Unsetenv(OidcDevMode)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr {
if err := ValidateIssuer(tt.args.issuer, tt.args.allowInsecure); (err != nil) != tt.wantErr {
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestValidateIssuerDevLocalAllowed(t *testing.T) {
func TestValidateIssuerPath(t *testing.T) {
type args struct {
issuer string
issuerPath *url.URL
}
tests := []struct {
name string
@ -82,17 +118,217 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) {
wantErr bool
}{
{
"localhost with http with dev ok",
args{"http://localhost:9999"},
"empty ok",
args{func() *url.URL {
u, _ := url.Parse("")
return u
}()},
false,
},
{
"custom ok",
args{func() *url.URL {
u, _ := url.Parse("/custom")
return u
}()},
false,
},
{
"fragment fails",
args{func() *url.URL {
u, _ := url.Parse("#fragment")
return u
}()},
true,
},
{
"query fails",
args{func() *url.URL {
u, _ := url.Parse("?query=value")
return u
}()},
true,
},
}
//nolint:errcheck
os.Setenv(OidcDevMode, "true")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr {
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
if err := ValidateIssuerPath(tt.args.issuerPath); (err != nil) != tt.wantErr {
t.Errorf("ValidateIssuerPath() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestIssuerFromHost(t *testing.T) {
type args struct {
path string
allowInsecure bool
target string
}
type res struct {
issuer string
err error
}
tests := []struct {
name string
args args
res res
}{
{
"invalid issuer path",
args{
path: "/#fragment",
allowInsecure: false,
},
res{
issuer: "",
err: ErrInvalidIssuerPath,
},
},
{
"empty path secure",
args{
path: "",
allowInsecure: false,
target: "https://issuer.com",
},
res{
issuer: "https://issuer.com",
err: nil,
},
},
{
"custom path secure",
args{
path: "/custom/",
allowInsecure: false,
target: "https://issuer.com",
},
res{
issuer: "https://issuer.com/custom/",
err: nil,
},
},
{
"custom path no leading slash",
args{
path: "custom/",
allowInsecure: false,
target: "https://issuer.com",
},
res{
issuer: "https://issuer.com/custom/",
err: nil,
},
},
{
"empty path unsecure",
args{
path: "",
allowInsecure: true,
target: "http://issuer.com",
},
res{
issuer: "http://issuer.com",
err: nil,
},
},
{
"custom path unsecure",
args{
path: "/custom/",
allowInsecure: true,
target: "http://issuer.com",
},
res{
issuer: "http://issuer.com/custom/",
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
issuer, err := IssuerFromHost(tt.args.path)(tt.args.allowInsecure)
if tt.res.err == nil {
assert.NoError(t, err)
req := httptest.NewRequest("", tt.args.target, nil)
assert.Equal(t, tt.res.issuer, issuer(req))
}
if tt.res.err != nil {
assert.ErrorIs(t, err, tt.res.err)
}
})
}
}
func TestStaticIssuer(t *testing.T) {
type args struct {
issuer string
allowInsecure bool
}
type res struct {
issuer string
err error
}
tests := []struct {
name string
args args
res res
}{
{
"invalid issuer",
args{
issuer: "",
allowInsecure: false,
},
res{
issuer: "",
err: ErrInvalidIssuerNoIssuer,
},
},
{
"empty path secure",
args{
issuer: "https://issuer.com",
allowInsecure: false,
},
res{
issuer: "https://issuer.com",
err: nil,
},
},
{
"custom path secure",
args{
issuer: "https://issuer.com/custom/",
allowInsecure: false,
},
res{
issuer: "https://issuer.com/custom/",
err: nil,
},
},
{
"unsecure",
args{
issuer: "http://issuer.com",
allowInsecure: true,
},
res{
issuer: "http://issuer.com",
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
issuer, err := StaticIssuer(tt.args.issuer)(tt.args.allowInsecure)
if tt.res.err == nil {
assert.NoError(t, err)
assert.Equal(t, tt.res.issuer, issuer(nil))
}
if tt.res.err != nil {
assert.ErrorIs(t, err, tt.res.err)
}
})
}

53
pkg/op/context.go Normal file
View file

@ -0,0 +1,53 @@
package op
import (
"context"
"net/http"
)
type key int
const (
issuerKey key = 0
)
type IssuerInterceptor struct {
issuerFromRequest IssuerFromRequest
}
// NewIssuerInterceptor will set the issuer into the context
// by the provided IssuerFromRequest (e.g. returned from StaticIssuer or IssuerFromHost)
func NewIssuerInterceptor(issuerFromRequest IssuerFromRequest) *IssuerInterceptor {
return &IssuerInterceptor{
issuerFromRequest: issuerFromRequest,
}
}
func (i *IssuerInterceptor) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
i.setIssuerCtx(w, r, next)
})
}
func (i *IssuerInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
i.setIssuerCtx(w, r, next)
}
}
// IssuerFromContext reads the issuer from the context (set by an IssuerInterceptor)
// it will return an empty string if not found
func IssuerFromContext(ctx context.Context) string {
ctxIssuer, _ := ctx.Value(issuerKey).(string)
return ctxIssuer
}
// ContextWithIssuer returns a new context with issuer set to it.
func ContextWithIssuer(ctx context.Context, issuer string) context.Context {
return context.WithValue(ctx, issuerKey, issuer)
}
func (i *IssuerInterceptor) setIssuerCtx(w http.ResponseWriter, r *http.Request, next http.Handler) {
r = r.WithContext(ContextWithIssuer(r.Context(), i.issuerFromRequest(r)))
next.ServeHTTP(w, r)
}

76
pkg/op/context_test.go Normal file
View file

@ -0,0 +1,76 @@
package op
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIssuerInterceptor(t *testing.T) {
type fields struct {
issuerFromRequest IssuerFromRequest
}
type args struct {
r *http.Request
next http.Handler
}
type res struct {
issuer string
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"empty",
fields{
func(r *http.Request) string {
return ""
},
},
args{},
res{
issuer: "",
},
},
{
"static",
fields{
func(r *http.Request) string {
return "static"
},
},
args{},
res{
issuer: "static",
},
},
{
"host",
fields{
func(r *http.Request) string {
return r.Host
},
},
args{},
res{
issuer: "issuer.com",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := NewIssuerInterceptor(tt.fields.issuerFromRequest)
next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
assert.Equal(t, tt.res.issuer, IssuerFromContext(r.Context()))
})
req := httptest.NewRequest("", "https://issuer.com", nil)
i.Handler(next).ServeHTTP(nil, req)
i.HandlerFunc(next).ServeHTTP(nil, req)
})
}
}

View file

@ -1,7 +1,7 @@
package op
import (
"github.com/zitadel/oidc/pkg/crypto"
"github.com/zitadel/oidc/v2/pkg/crypto"
)
type Crypto interface {

265
pkg/op/device.go Normal file
View file

@ -0,0 +1,265 @@
package op
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"math/big"
"net/http"
"strings"
"time"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type DeviceAuthorizationConfig struct {
Lifetime time.Duration
PollInterval time.Duration
UserFormURL string // the URL where the user must go to authorize the device
UserCode UserCodeConfig
}
type UserCodeConfig struct {
CharSet string
CharAmount int
DashInterval int
}
const (
CharSetBase20 = "BCDFGHJKLMNPQRSTVWXZ"
CharSetDigits = "0123456789"
)
var (
UserCodeBase20 = UserCodeConfig{
CharSet: CharSetBase20,
CharAmount: 8,
DashInterval: 4,
}
UserCodeDigits = UserCodeConfig{
CharSet: CharSetDigits,
CharAmount: 9,
DashInterval: 3,
}
)
func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if err := DeviceAuthorization(w, r, o); err != nil {
RequestError(w, r, err)
}
}
}
func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error {
storage, err := assertDeviceStorage(o.Storage())
if err != nil {
return err
}
req, err := ParseDeviceCodeRequest(r, o)
if err != nil {
return err
}
config := o.DeviceAuthorization()
deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes)
if err != nil {
return err
}
userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
if err != nil {
return err
}
expires := time.Now().Add(config.Lifetime)
err = storage.StoreDeviceAuthorization(r.Context(), req.ClientID, deviceCode, userCode, expires, req.Scopes)
if err != nil {
return err
}
response := &oidc.DeviceAuthorizationResponse{
DeviceCode: deviceCode,
UserCode: userCode,
VerificationURI: config.UserFormURL,
ExpiresIn: int(config.Lifetime / time.Second),
Interval: int(config.PollInterval / time.Second),
}
response.VerificationURIComplete = fmt.Sprintf("%s?user_code=%s", config.UserFormURL, userCode)
httphelper.MarshalJSON(w, response)
return nil
}
func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) {
clientID, _, err := ClientIDFromRequest(r, o)
if err != nil {
return nil, err
}
req := new(oidc.DeviceAuthorizationRequest)
if err := o.Decoder().Decode(req, r.Form); err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse device authentication request").WithParent(err)
}
req.ClientID = clientID
return req, nil
}
// 16 bytes gives 128 bit of entropy.
// results in a 22 character base64 encoded string.
const RecommendedDeviceCodeBytes = 16
func NewDeviceCode(nBytes int) (string, error) {
bytes := make([]byte, nBytes)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("%w getting entropy for device code", err)
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) {
var buf strings.Builder
if dashInterval > 0 {
buf.Grow(charAmount + charAmount/dashInterval - 1)
} else {
buf.Grow(charAmount)
}
max := big.NewInt(int64(len(charSet)))
for i := 0; i < charAmount; i++ {
if dashInterval != 0 && i != 0 && i%dashInterval == 0 {
buf.WriteByte('-')
}
bi, err := rand.Int(rand.Reader, max)
if err != nil {
return "", fmt.Errorf("%w getting entropy for user code", err)
}
buf.WriteRune(charSet[int(bi.Int64())])
}
return buf.String(), nil
}
type deviceAccessTokenRequest struct {
subject string
audience []string
scopes []string
}
func (r *deviceAccessTokenRequest) GetSubject() string {
return r.subject
}
func (r *deviceAccessTokenRequest) GetAudience() []string {
return r.audience
}
func (r *deviceAccessTokenRequest) GetScopes() []string {
return r.scopes
}
func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
if err := deviceAccessToken(w, r, exchanger); err != nil {
RequestError(w, r, err)
}
}
func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) error {
// use a limited context timeout shorter as the default
// poll interval of 5 seconds.
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
defer cancel()
r = r.WithContext(ctx)
clientID, clientAuthenticated, err := ClientIDFromRequest(r, exchanger)
if err != nil {
return err
}
req, err := ParseDeviceAccessTokenRequest(r, exchanger)
if err != nil {
return err
}
state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger)
if err != nil {
return err
}
client, err := exchanger.Storage().GetClientByClientID(ctx, clientID)
if err != nil {
return err
}
if clientAuthenticated != IsConfidentialType(client) {
return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials).
WithDescription("confidential client requires authentication")
}
tokenRequest := &deviceAccessTokenRequest{
subject: state.Subject,
audience: []string{clientID},
scopes: state.Scopes,
}
resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client)
if err != nil {
return err
}
httphelper.MarshalJSON(w, resp)
return nil
}
func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.DeviceAccessTokenRequest, error) {
req := new(oidc.DeviceAccessTokenRequest)
if err := exchanger.Decoder().Decode(req, r.PostForm); err != nil {
return nil, err
}
return req, nil
}
func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
storage, err := assertDeviceStorage(exchanger.Storage())
if err != nil {
return nil, err
}
state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode)
if errors.Is(err, context.DeadlineExceeded) {
return nil, oidc.ErrSlowDown().WithParent(err)
}
if err != nil {
return nil, oidc.ErrAccessDenied().WithParent(err)
}
if state.Denied {
return state, oidc.ErrAccessDenied()
}
if state.Done {
return state, nil
}
if time.Now().After(state.Expires) {
return state, oidc.ErrExpiredDeviceCode()
}
return state, oidc.ErrAuthorizationPending()
}
func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client AccessTokenClient) (*oidc.AccessTokenResponse, error) {
accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, client, "")
if err != nil {
return nil, err
}
return &oidc.AccessTokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: oidc.BearerToken,
ExpiresIn: uint64(validity.Seconds()),
}, nil
}

407
pkg/op/device_test.go Normal file
View file

@ -0,0 +1,407 @@
package op_test
import (
"context"
"crypto/rand"
"encoding/base64"
"io"
mr "math/rand"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
)
func Test_deviceAuthorizationHandler(t *testing.T) {
req := &oidc.DeviceAuthorizationRequest{
Scopes: []string{"foo", "bar"},
ClientID: "web",
}
values := make(url.Values)
testProvider.Encoder().Encode(req, values)
body := strings.NewReader(values.Encode())
r := httptest.NewRequest(http.MethodPost, "/", body)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
runWithRandReader(mr.New(mr.NewSource(1)), func() {
op.DeviceAuthorizationHandler(testProvider)(w, r)
})
result := w.Result()
assert.Less(t, result.StatusCode, 300)
got, _ := io.ReadAll(result.Body)
assert.JSONEq(t, `{"device_code":"Uv38ByGCZU8WP18PmmIdcg", "expires_in":300, "interval":5, "user_code":"JKRV-FRGK", "verification_uri":"https://localhost:9998/device", "verification_uri_complete":"https://localhost:9998/device?user_code=JKRV-FRGK"}`, string(got))
}
func TestParseDeviceCodeRequest(t *testing.T) {
tests := []struct {
name string
req *oidc.DeviceAuthorizationRequest
wantErr bool
}{
{
name: "empty request",
wantErr: true,
},
{
name: "success",
req: &oidc.DeviceAuthorizationRequest{
Scopes: oidc.SpaceDelimitedArray{"foo", "bar"},
ClientID: "web",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var body io.Reader
if tt.req != nil {
values := make(url.Values)
testProvider.Encoder().Encode(tt.req, values)
body = strings.NewReader(values.Encode())
}
r := httptest.NewRequest(http.MethodPost, "/", body)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
got, err := op.ParseDeviceCodeRequest(r, testProvider)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.req, got)
})
}
}
func runWithRandReader(r io.Reader, f func()) {
originalReader := rand.Reader
rand.Reader = r
defer func() {
rand.Reader = originalReader
}()
f()
}
func TestNewDeviceCode(t *testing.T) {
t.Run("reader error", func(t *testing.T) {
runWithRandReader(errReader{}, func() {
_, err := op.NewDeviceCode(16)
require.Error(t, err)
})
})
t.Run("different lengths, rand reader", func(t *testing.T) {
for i := 1; i <= 32; i++ {
got, err := op.NewDeviceCode(i)
require.NoError(t, err)
assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
}
})
}
func TestNewUserCode(t *testing.T) {
type args struct {
charset []rune
charAmount int
dashInterval int
}
tests := []struct {
name string
args args
reader io.Reader
want string
wantErr bool
}{
{
name: "reader error",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: errReader{},
wantErr: true,
},
{
name: "base20",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: mr.New(mr.NewSource(1)),
want: "XKCD-HTTD",
},
{
name: "digits",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
dashInterval: 3,
},
reader: mr.New(mr.NewSource(1)),
want: "271-256-225",
},
{
name: "no dashes",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
},
reader: mr.New(mr.NewSource(1)),
want: "271256225",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runWithRandReader(tt.reader, func() {
got, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
if tt.wantErr {
require.ErrorIs(t, err, io.ErrNoProgress)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, got)
})
})
}
t.Run("crypto/rand", func(t *testing.T) {
const testN = 100000
for _, c := range []op.UserCodeConfig{op.UserCodeBase20, op.UserCodeDigits} {
t.Run(c.CharSet, func(t *testing.T) {
results := make(map[string]int)
for i := 0; i < testN; i++ {
code, err := op.NewUserCode([]rune(c.CharSet), c.CharAmount, c.DashInterval)
require.NoError(t, err)
results[code]++
}
t.Log(results)
var duplicates int
for code, count := range results {
assert.Less(t, count, 3, code)
if count == 2 {
duplicates++
}
}
})
}
})
}
func BenchmarkNewUserCode(b *testing.B) {
type args struct {
charset []rune
charAmount int
dashInterval int
}
tests := []struct {
name string
args args
reader io.Reader
}{
{
name: "math rand, base20",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: mr.New(mr.NewSource(1)),
},
{
name: "math rand, digits",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
dashInterval: 3,
},
reader: mr.New(mr.NewSource(1)),
},
{
name: "crypto rand, base20",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: rand.Reader,
},
{
name: "crypto rand, digits",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
dashInterval: 3,
},
reader: rand.Reader,
},
}
for _, tt := range tests {
runWithRandReader(tt.reader, func() {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
require.NoError(b, err)
}
})
})
}
}
func TestDeviceAccessToken(t *testing.T) {
storage := testProvider.Storage().(op.DeviceAuthorizationStorage)
storage.StoreDeviceAuthorization(context.Background(), "native", "qwerty", "yuiop", time.Now().Add(time.Minute), []string{"foo"})
storage.CompleteDeviceAuthorization(context.Background(), "yuiop", "tim")
values := make(url.Values)
values.Set("client_id", "native")
values.Set("grant_type", string(oidc.GrantTypeDeviceCode))
values.Set("device_code", "qwerty")
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(values.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
op.DeviceAccessToken(w, r, testProvider)
result := w.Result()
got, _ := io.ReadAll(result.Body)
t.Log(string(got))
assert.Less(t, result.StatusCode, 300)
assert.NotEmpty(t, string(got))
}
func TestCheckDeviceAuthorizationState(t *testing.T) {
now := time.Now()
storage := testProvider.Storage().(op.DeviceAuthorizationStorage)
storage.StoreDeviceAuthorization(context.Background(), "native", "pending", "pending", now.Add(time.Minute), []string{"foo"})
storage.StoreDeviceAuthorization(context.Background(), "native", "denied", "denied", now.Add(time.Minute), []string{"foo"})
storage.StoreDeviceAuthorization(context.Background(), "native", "completed", "completed", now.Add(time.Minute), []string{"foo"})
storage.StoreDeviceAuthorization(context.Background(), "native", "expired", "expired", now.Add(-time.Minute), []string{"foo"})
storage.DenyDeviceAuthorization(context.Background(), "denied")
storage.CompleteDeviceAuthorization(context.Background(), "completed", "tim")
exceededCtx, cancel := context.WithTimeout(context.Background(), -time.Second)
defer cancel()
type args struct {
ctx context.Context
clientID string
deviceCode string
}
tests := []struct {
name string
args args
want *op.DeviceAuthorizationState
wantErr error
}{
{
name: "pending",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "pending",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(time.Minute),
},
wantErr: oidc.ErrAuthorizationPending(),
},
{
name: "slow down",
args: args{
ctx: exceededCtx,
clientID: "native",
deviceCode: "ok",
},
wantErr: oidc.ErrSlowDown(),
},
{
name: "wrong client",
args: args{
ctx: context.Background(),
clientID: "foo",
deviceCode: "ok",
},
wantErr: oidc.ErrAccessDenied(),
},
{
name: "denied",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "denied",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(time.Minute),
Denied: true,
},
wantErr: oidc.ErrAccessDenied(),
},
{
name: "completed",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "completed",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(time.Minute),
Subject: "tim",
Done: true,
},
},
{
name: "expired",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "expired",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(-time.Minute),
},
wantErr: oidc.ErrExpiredDeviceCode(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := op.CheckDeviceAuthorizationState(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, testProvider)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -1,49 +1,17 @@
package op
import (
"context"
"net/http"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
"gopkg.in/square/go-jose.v2"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Discover(w, CreateDiscoveryConfig(c, s))
}
}
func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
httphelper.MarshalJSON(w, config)
}
func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration {
return &oidc.DiscoveryConfiguration{
Issuer: c.Issuer(),
AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()),
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()),
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
ScopesSupported: Scopes(c),
ResponseTypesSupported: ResponseTypes(c),
GrantTypesSupported: GrantTypes(c),
SubjectTypesSupported: SubjectTypes(c),
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(c),
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c),
TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(c),
IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(c),
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c),
RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(c),
RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(c),
ClaimsSupported: SupportedClaims(c),
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
UILocalesSupported: c.SupportedUILocales(),
RequestParameterSupported: c.RequestObjectSupported(),
}
type DiscoverStorage interface {
SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error)
}
var DefaultSupportedScopes = []string{
@ -55,6 +23,47 @@ var DefaultSupportedScopes = []string{
oidc.ScopeOfflineAccess,
}
func discoveryHandler(c Configuration, s DiscoverStorage) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Discover(w, CreateDiscoveryConfig(r, c, s))
}
}
func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
httphelper.MarshalJSON(w, config)
}
func CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration {
issuer := config.IssuerFromRequest(r)
return &oidc.DiscoveryConfiguration{
Issuer: issuer,
AuthorizationEndpoint: config.AuthorizationEndpoint().Absolute(issuer),
TokenEndpoint: config.TokenEndpoint().Absolute(issuer),
IntrospectionEndpoint: config.IntrospectionEndpoint().Absolute(issuer),
UserinfoEndpoint: config.UserinfoEndpoint().Absolute(issuer),
RevocationEndpoint: config.RevocationEndpoint().Absolute(issuer),
EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
JwksURI: config.KeysEndpoint().Absolute(issuer),
DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
ScopesSupported: Scopes(config),
ResponseTypesSupported: ResponseTypes(config),
GrantTypesSupported: GrantTypes(config),
SubjectTypesSupported: SubjectTypes(config),
IDTokenSigningAlgValuesSupported: SigAlgorithms(r.Context(), storage),
RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config),
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config),
TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config),
IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(config),
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(config),
RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(config),
RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(config),
ClaimsSupported: SupportedClaims(config),
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
UILocalesSupported: config.SupportedUILocales(),
RequestParameterSupported: config.RequestObjectSupported(),
}
}
func Scopes(c Configuration) []string {
return DefaultSupportedScopes // TODO: config
}
@ -84,9 +93,94 @@ func GrantTypes(c Configuration) []oidc.GrantType {
if c.GrantTypeJWTAuthorizationSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
}
if c.GrantTypeDeviceCodeSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeDeviceCode)
}
return grantTypes
}
func SubjectTypes(c Configuration) []string {
return []string{"public"} //TODO: config
}
func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
algorithms, err := storage.SignatureAlgorithms(ctx)
if err != nil {
return nil
}
algs := make([]string, len(algorithms))
for i, algorithm := range algorithms {
algs[i] = string(algorithm)
}
return algs
}
func RequestObjectSigAlgorithms(c Configuration) []string {
if !c.RequestObjectSupported() {
return nil
}
return c.RequestObjectSigningAlgorithmsSupported()
}
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone,
oidc.AuthMethodBasic,
}
if c.AuthMethodPostSupported() {
authMethods = append(authMethods, oidc.AuthMethodPost)
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func TokenSigAlgorithms(c Configuration) []string {
if !c.AuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.TokenEndpointSigningAlgorithmsSupported()
}
func IntrospectionSigAlgorithms(c Configuration) []string {
if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.IntrospectionEndpointSigningAlgorithmsSupported()
}
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodBasic,
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func RevocationSigAlgorithms(c Configuration) []string {
if !c.RevocationAuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.RevocationEndpointSigningAlgorithmsSupported()
}
func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone,
oidc.AuthMethodBasic,
}
if c.AuthMethodPostSupported() {
authMethods = append(authMethods, oidc.AuthMethodPost)
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func SupportedClaims(c Configuration) []string {
return []string{ // TODO: config
"sub",
@ -116,59 +210,6 @@ func SupportedClaims(c Configuration) []string {
}
}
func SigAlgorithms(s Signer) []string {
return []string{string(s.SignatureAlgorithm())}
}
func SubjectTypes(c Configuration) []string {
return []string{"public"} // TODO: config
}
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone,
oidc.AuthMethodBasic,
}
if c.AuthMethodPostSupported() {
authMethods = append(authMethods, oidc.AuthMethodPost)
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func TokenSigAlgorithms(c Configuration) []string {
if !c.AuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.TokenEndpointSigningAlgorithmsSupported()
}
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodBasic,
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone,
oidc.AuthMethodBasic,
}
if c.AuthMethodPostSupported() {
authMethods = append(authMethods, oidc.AuthMethodPost)
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
if c.CodeMethodS256Supported() {
@ -176,24 +217,3 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
}
return codeMethods
}
func IntrospectionSigAlgorithms(c Configuration) []string {
if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.IntrospectionEndpointSigningAlgorithmsSupported()
}
func RevocationSigAlgorithms(c Configuration) []string {
if !c.RevocationAuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.RevocationEndpointSigningAlgorithmsSupported()
}
func RequestObjectSigAlgorithms(c Configuration) []string {
if !c.RequestObjectSupported() {
return nil
}
return c.RequestObjectSigningAlgorithmsSupported()
}

View file

@ -1,18 +1,19 @@
package op_test
import (
"context"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/pkg/op/mock"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/v2/pkg/op/mock"
)
func TestDiscover(t *testing.T) {
@ -47,8 +48,9 @@ func TestDiscover(t *testing.T) {
func TestCreateDiscoveryConfig(t *testing.T) {
type args struct {
c op.Configuration
s op.Signer
request *http.Request
c op.Configuration
s op.DiscoverStorage
}
tests := []struct {
name string
@ -59,9 +61,8 @@ func TestCreateDiscoveryConfig(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.CreateDiscoveryConfig(tt.args.c, tt.args.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateDiscoveryConfig() = %v, want %v", got, tt.want)
}
got := op.CreateDiscoveryConfig(tt.args.request, tt.args.c, tt.args.s)
assert.Equal(t, tt.want, got)
})
}
}
@ -83,9 +84,8 @@ func Test_scopes(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.Scopes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("scopes() = %v, want %v", got, tt.want)
}
got := op.Scopes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
@ -99,13 +99,16 @@ func Test_ResponseTypes(t *testing.T) {
args args
want []string
}{
// TODO: Add test cases.
{
"code and implicit flow",
args{},
[]string{"code", "id_token", "id_token token"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.ResponseTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("responseTypes() = %v, want %v", got, tt.want)
}
got := op.ResponseTypes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
@ -117,63 +120,53 @@ func Test_GrantTypes(t *testing.T) {
tests := []struct {
name string
args args
want []string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.GrantTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("grantTypes() = %v, want %v", got, tt.want)
}
})
}
}
func TestSupportedClaims(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.SupportedClaims(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("SupportedClaims() = %v, want %v", got, tt.want)
}
})
}
}
func Test_SigAlgorithms(t *testing.T) {
m := mock.NewMockSigner(gomock.NewController(t))
type args struct {
s op.Signer
}
tests := []struct {
name string
args args
want []string
want []oidc.GrantType
}{
{
"",
args{func() op.Signer {
m.EXPECT().SignatureAlgorithm().Return(jose.RS256)
return m
}()},
[]string{"RS256"},
"code and implicit flow",
args{
func() op.Configuration {
c := mock.NewMockConfiguration(gomock.NewController(t))
c.EXPECT().GrantTypeRefreshTokenSupported().Return(false)
c.EXPECT().GrantTypeTokenExchangeSupported().Return(false)
c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(false)
c.EXPECT().GrantTypeClientCredentialsSupported().Return(false)
c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
return c
}(),
},
[]oidc.GrantType{
oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
},
},
{
"code, implicit flow, refresh token, token exchange, jwt profile, client_credentials",
args{
func() op.Configuration {
c := mock.NewMockConfiguration(gomock.NewController(t))
c.EXPECT().GrantTypeRefreshTokenSupported().Return(true)
c.EXPECT().GrantTypeTokenExchangeSupported().Return(true)
c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(true)
c.EXPECT().GrantTypeClientCredentialsSupported().Return(true)
c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
return c
}(),
},
[]oidc.GrantType{
oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
oidc.GrantTypeRefreshToken,
oidc.GrantTypeClientCredentials,
oidc.GrantTypeTokenExchange,
oidc.GrantTypeBearer,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.SigAlgorithms(tt.args.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("sigAlgorithms() = %v, want %v", got, tt.want)
}
got := op.GrantTypes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
@ -195,9 +188,80 @@ func Test_SubjectTypes(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.SubjectTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("subjectTypes() = %v, want %v", got, tt.want)
}
got := op.SubjectTypes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_SigAlgorithms(t *testing.T) {
m := mock.NewMockDiscoverStorage(gomock.NewController(t))
type args struct {
s op.DiscoverStorage
}
tests := []struct {
name string
args args
want []string
}{
{
"",
args{func() op.DiscoverStorage {
m.EXPECT().SignatureAlgorithms(gomock.Any()).Return([]jose.SignatureAlgorithm{jose.RS256}, nil)
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.SigAlgorithms(context.Background(), tt.args.s)
assert.Equal(t, tt.want, got)
})
}
}
func Test_RequestObjectSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().RequestObjectSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().RequestObjectSupported().Return(true)
m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().RequestObjectSupported().Return(true)
m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.RequestObjectSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
@ -244,9 +308,311 @@ func Test_AuthMethodsTokenEndpoint(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("authMethods() = %v, want %v", got, tt.want)
}
got := op.AuthMethodsTokenEndpoint(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_TokenSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.TokenSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_IntrospectionSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.IntrospectionSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_AuthMethodsIntrospectionEndpoint(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.AuthMethod
}{
{
"basic only",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodBasic},
},
{
"basic and private_key_jwt",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodBasic, oidc.AuthMethodPrivateKeyJWT},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.AuthMethodsIntrospectionEndpoint(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_RevocationSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.RevocationSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_AuthMethodsRevocationEndpoint(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.AuthMethod
}{
{
"none and basic",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(false)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic},
},
{
"none, basic and post",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(true)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost},
},
{
"none, basic, post and private_key_jwt",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(true)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.AuthMethodsRevocationEndpoint(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func TestSupportedClaims(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"scopes",
args{},
[]string{
"sub",
"aud",
"exp",
"iat",
"iss",
"auth_time",
"nonce",
"acr",
"amr",
"c_hash",
"at_hash",
"act",
"scopes",
"client_id",
"azp",
"preferred_username",
"name",
"family_name",
"given_name",
"locale",
"email",
"email_verified",
"phone_number",
"phone_number_verified",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.SupportedClaims(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_CodeChallengeMethods(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.CodeChallengeMethod
}{
{
"not supported",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().CodeMethodS256Supported().Return(false)
return m
}()},
[]oidc.CodeChallengeMethod{},
},
{
"S256",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().CodeMethodS256Supported().Return(true)
return m
}()},
[]oidc.CodeChallengeMethod{oidc.CodeChallengeMethodS256},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.CodeChallengeMethods(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -3,7 +3,7 @@ package op_test
import (
"testing"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/v2/pkg/op"
)
func TestEndpoint_Path(t *testing.T) {

View file

@ -3,8 +3,8 @@ package op
import (
"net/http"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type ErrAuthRequest interface {

View file

@ -6,11 +6,11 @@ import (
"gopkg.in/square/go-jose.v2"
httphelper "github.com/zitadel/oidc/pkg/http"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
)
type KeyProvider interface {
GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
KeySet(context.Context) ([]Key, error)
}
func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
@ -20,10 +20,23 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
}
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
keySet, err := k.GetKeySet(r.Context())
keySet, err := k.KeySet(r.Context())
if err != nil {
httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError)
return
}
httphelper.MarshalJSON(w, keySet)
httphelper.MarshalJSON(w, jsonWebKeySet(keySet))
}
func jsonWebKeySet(keys []Key) *jose.JSONWebKeySet {
webKeys := make([]jose.JSONWebKey, len(keys))
for i, key := range keys {
webKeys[i] = jose.JSONWebKey{
KeyID: key.ID(),
Algorithm: string(key.Algorithm()),
Use: key.Use(),
Key: key.Key(),
}
}
return &jose.JSONWebKeySet{Keys: webKeys}
}

View file

@ -11,9 +11,9 @@ import (
"github.com/stretchr/testify/assert"
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/pkg/op/mock"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/v2/pkg/op/mock"
)
func TestKeys(t *testing.T) {
@ -35,7 +35,7 @@ func TestKeys(t *testing.T) {
args: args{
k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t))
m.EXPECT().GetKeySet(gomock.Any()).Return(nil, oidc.ErrServerError())
m.EXPECT().KeySet(gomock.Any()).Return(nil, oidc.ErrServerError())
return m
}(),
},
@ -51,39 +51,39 @@ func TestKeys(t *testing.T) {
args: args{
k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t))
m.EXPECT().GetKeySet(gomock.Any()).Return(nil, nil)
m.EXPECT().KeySet(gomock.Any()).Return(nil, nil)
return m
}(),
},
res: res{
statusCode: http.StatusOK,
contentType: "application/json",
body: `{"keys":[]}
`,
},
},
{
name: "list",
args: args{
k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t))
m.EXPECT().GetKeySet(gomock.Any()).Return(
&jose.JSONWebKeySet{Keys: []jose.JSONWebKey{
{
Key: &rsa.PublicKey{
N: big.NewInt(1),
E: 1,
},
KeyID: "id",
},
}},
nil,
)
ctrl := gomock.NewController(t)
m := mock.NewMockKeyProvider(ctrl)
k := mock.NewMockKey(ctrl)
k.EXPECT().Key().Return(&rsa.PublicKey{
N: big.NewInt(1),
E: 1,
})
k.EXPECT().ID().Return("id")
k.EXPECT().Algorithm().Return(jose.RS256)
k.EXPECT().Use().Return("sig")
m.EXPECT().KeySet(gomock.Any()).Return([]op.Key{k}, nil)
return m
}(),
},
res: res{
statusCode: http.StatusOK,
contentType: "application/json",
body: `{"keys":[{"kty":"RSA","kid":"id","n":"AQ","e":"AQ"}]}
body: `{"keys":[{"use":"sig","kty":"RSA","kid":"id","alg":"RS256","n":"AQ","e":"AQ"}]}
`,
},
},

View file

@ -1,15 +1,16 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Authorizer)
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Authorizer)
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
http "github.com/zitadel/oidc/pkg/http"
op "github.com/zitadel/oidc/pkg/op"
http "github.com/zitadel/oidc/v2/pkg/http"
op "github.com/zitadel/oidc/v2/pkg/op"
)
// MockAuthorizer is a mock of Authorizer interface.
@ -78,31 +79,17 @@ func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call {
}
// IDTokenHintVerifier mocks base method.
func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier {
func (m *MockAuthorizer) IDTokenHintVerifier(arg0 context.Context) op.IDTokenHintVerifier {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IDTokenHintVerifier")
ret := m.ctrl.Call(m, "IDTokenHintVerifier", arg0)
ret0, _ := ret[0].(op.IDTokenHintVerifier)
return ret0
}
// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier.
func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier() *gomock.Call {
func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier))
}
// Issuer mocks base method.
func (m *MockAuthorizer) Issuer() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Issuer")
ret0, _ := ret[0].(string)
return ret0
}
// Issuer indicates an expected call of Issuer.
func (mr *MockAuthorizerMockRecorder) Issuer() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockAuthorizer)(nil).Issuer))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier), arg0)
}
// RequestObjectSupported mocks base method.
@ -119,20 +106,6 @@ func (mr *MockAuthorizerMockRecorder) RequestObjectSupported() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSupported", reflect.TypeOf((*MockAuthorizer)(nil).RequestObjectSupported))
}
// Signer mocks base method.
func (m *MockAuthorizer) Signer() op.Signer {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Signer")
ret0, _ := ret[0].(op.Signer)
return ret0
}
// Signer indicates an expected call of Signer.
func (mr *MockAuthorizerMockRecorder) Signer() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockAuthorizer)(nil).Signer))
}
// Storage mocks base method.
func (m *MockAuthorizer) Storage() op.Storage {
m.ctrl.T.Helper()

View file

@ -8,8 +8,8 @@ import (
"github.com/gorilla/schema"
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
)
func NewAuthorizer(t *testing.T) op.Authorizer {
@ -20,23 +20,13 @@ func NewAuthorizerExpectValid(t *testing.T, wantErr bool) op.Authorizer {
m := NewAuthorizer(t)
ExpectDecoder(m)
ExpectEncoder(m)
ExpectSigner(m, t)
//ExpectSigner(m, t)
ExpectStorage(m, t)
ExpectVerifier(m, t)
// ExpectErrorHandler(m, t, wantErr)
return m
}
// func NewAuthorizerExpectDecoderFails(t *testing.T) op.Authorizer {
// m := NewAuthorizer(t)
// ExpectDecoderFails(m)
// ExpectEncoder(m)
// ExpectSigner(m, t)
// ExpectStorage(m, t)
// ExpectErrorHandler(m, t)
// return m
// }
func ExpectDecoder(a op.Authorizer) {
mockA := a.(*MockAuthorizer)
mockA.EXPECT().Decoder().AnyTimes().Return(schema.NewDecoder())
@ -47,17 +37,18 @@ func ExpectEncoder(a op.Authorizer) {
mockA.EXPECT().Encoder().AnyTimes().Return(schema.NewEncoder())
}
func ExpectSigner(a op.Authorizer, t *testing.T) {
mockA := a.(*MockAuthorizer)
mockA.EXPECT().Signer().DoAndReturn(
func() op.Signer {
return &Sig{}
})
}
//
//func ExpectSigner(a op.Authorizer, t *testing.T) {
// mockA := a.(*MockAuthorizer)
// mockA.EXPECT().Signer().DoAndReturn(
// func() op.Signer {
// return &Sig{}
// })
//}
func ExpectVerifier(a op.Authorizer, t *testing.T) {
mockA := a.(*MockAuthorizer)
mockA.EXPECT().IDTokenHintVerifier().DoAndReturn(
mockA.EXPECT().IDTokenHintVerifier(gomock.Any()).DoAndReturn(
func() op.IDTokenHintVerifier {
return op.NewIDTokenHintVerifier("", nil)
})

View file

@ -5,8 +5,8 @@ import (
"github.com/golang/mock/gomock"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
)
func NewClient(t *testing.T) op.Client {

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Client)
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Client)
// Package mock is a generated GoMock package.
package mock
@ -9,8 +9,8 @@ import (
time "time"
gomock "github.com/golang/mock/gomock"
oidc "github.com/zitadel/oidc/pkg/oidc"
op "github.com/zitadel/oidc/pkg/op"
oidc "github.com/zitadel/oidc/v2/pkg/oidc"
op "github.com/zitadel/oidc/v2/pkg/op"
)
// MockClient is a mock of Client interface.

View file

@ -1,14 +1,15 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Configuration)
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Configuration)
// Package mock is a generated GoMock package.
package mock
import (
http "net/http"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
op "github.com/zitadel/oidc/pkg/op"
op "github.com/zitadel/oidc/v2/pkg/op"
language "golang.org/x/text/language"
)
@ -91,6 +92,34 @@ func (mr *MockConfigurationMockRecorder) CodeMethodS256Supported() *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeMethodS256Supported", reflect.TypeOf((*MockConfiguration)(nil).CodeMethodS256Supported))
}
// DeviceAuthorization mocks base method.
func (m *MockConfiguration) DeviceAuthorization() op.DeviceAuthorizationConfig {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeviceAuthorization")
ret0, _ := ret[0].(op.DeviceAuthorizationConfig)
return ret0
}
// DeviceAuthorization indicates an expected call of DeviceAuthorization.
func (mr *MockConfigurationMockRecorder) DeviceAuthorization() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorization", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorization))
}
// DeviceAuthorizationEndpoint mocks base method.
func (m *MockConfiguration) DeviceAuthorizationEndpoint() op.Endpoint {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeviceAuthorizationEndpoint")
ret0, _ := ret[0].(op.Endpoint)
return ret0
}
// DeviceAuthorizationEndpoint indicates an expected call of DeviceAuthorizationEndpoint.
func (mr *MockConfigurationMockRecorder) DeviceAuthorizationEndpoint() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorizationEndpoint))
}
// EndSessionEndpoint mocks base method.
func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint {
m.ctrl.T.Helper()
@ -119,6 +148,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeClientCredentialsSupported() *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeClientCredentialsSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeClientCredentialsSupported))
}
// GrantTypeDeviceCodeSupported mocks base method.
func (m *MockConfiguration) GrantTypeDeviceCodeSupported() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GrantTypeDeviceCodeSupported")
ret0, _ := ret[0].(bool)
return ret0
}
// GrantTypeDeviceCodeSupported indicates an expected call of GrantTypeDeviceCodeSupported.
func (mr *MockConfigurationMockRecorder) GrantTypeDeviceCodeSupported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeDeviceCodeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeDeviceCodeSupported))
}
// GrantTypeJWTAuthorizationSupported mocks base method.
func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool {
m.ctrl.T.Helper()
@ -161,6 +204,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported))
}
// Insecure mocks base method.
func (m *MockConfiguration) Insecure() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insecure")
ret0, _ := ret[0].(bool)
return ret0
}
// Insecure indicates an expected call of Insecure.
func (mr *MockConfigurationMockRecorder) Insecure() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insecure", reflect.TypeOf((*MockConfiguration)(nil).Insecure))
}
// IntrospectionAuthMethodPrivateKeyJWTSupported mocks base method.
func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
m.ctrl.T.Helper()
@ -203,18 +260,18 @@ func (mr *MockConfigurationMockRecorder) IntrospectionEndpointSigningAlgorithmsS
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported))
}
// Issuer mocks base method.
func (m *MockConfiguration) Issuer() string {
// IssuerFromRequest mocks base method.
func (m *MockConfiguration) IssuerFromRequest(arg0 *http.Request) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Issuer")
ret := m.ctrl.Call(m, "IssuerFromRequest", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// Issuer indicates an expected call of Issuer.
func (mr *MockConfigurationMockRecorder) Issuer() *gomock.Call {
// IssuerFromRequest indicates an expected call of IssuerFromRequest.
func (mr *MockConfigurationMockRecorder) IssuerFromRequest(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockConfiguration)(nil).Issuer))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssuerFromRequest", reflect.TypeOf((*MockConfiguration)(nil).IssuerFromRequest), arg0)
}
// KeysEndpoint mocks base method.

View file

@ -0,0 +1,51 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: DiscoverStorage)
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
jose "gopkg.in/square/go-jose.v2"
)
// MockDiscoverStorage is a mock of DiscoverStorage interface.
type MockDiscoverStorage struct {
ctrl *gomock.Controller
recorder *MockDiscoverStorageMockRecorder
}
// MockDiscoverStorageMockRecorder is the mock recorder for MockDiscoverStorage.
type MockDiscoverStorageMockRecorder struct {
mock *MockDiscoverStorage
}
// NewMockDiscoverStorage creates a new mock instance.
func NewMockDiscoverStorage(ctrl *gomock.Controller) *MockDiscoverStorage {
mock := &MockDiscoverStorage{ctrl: ctrl}
mock.recorder = &MockDiscoverStorageMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDiscoverStorage) EXPECT() *MockDiscoverStorageMockRecorder {
return m.recorder
}
// SignatureAlgorithms mocks base method.
func (m *MockDiscoverStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0)
ret0, _ := ret[0].([]jose.SignatureAlgorithm)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SignatureAlgorithms indicates an expected call of SignatureAlgorithms.
func (mr *MockDiscoverStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockDiscoverStorage)(nil).SignatureAlgorithms), arg0)
}

View file

@ -1,8 +1,10 @@
package mock
//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/pkg/op Storage
//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/pkg/op Authorizer
//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/pkg/op Client
//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/pkg/op Configuration
//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/pkg/op Signer
//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/pkg/op KeyProvider
//go:generate go install github.com/golang/mock/mockgen@v1.6.0
//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v2/pkg/op Storage
//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v2/pkg/op Authorizer
//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v2/pkg/op Client
//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v2/pkg/op Configuration
//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v2/pkg/op DiscoverStorage
//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v2/pkg/op SigningKey,Key
//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/v2/pkg/op KeyProvider

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/pkg/op (interfaces: KeyProvider)
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: KeyProvider)
// Package mock is a generated GoMock package.
package mock
@ -9,7 +9,7 @@ import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
jose "gopkg.in/square/go-jose.v2"
op "github.com/zitadel/oidc/v2/pkg/op"
)
// MockKeyProvider is a mock of KeyProvider interface.
@ -35,17 +35,17 @@ func (m *MockKeyProvider) EXPECT() *MockKeyProviderMockRecorder {
return m.recorder
}
// GetKeySet mocks base method.
func (m *MockKeyProvider) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
// KeySet mocks base method.
func (m *MockKeyProvider) KeySet(arg0 context.Context) ([]op.Key, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetKeySet", arg0)
ret0, _ := ret[0].(*jose.JSONWebKeySet)
ret := m.ctrl.Call(m, "KeySet", arg0)
ret0, _ := ret[0].([]op.Key)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetKeySet indicates an expected call of GetKeySet.
func (mr *MockKeyProviderMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call {
// KeySet indicates an expected call of KeySet.
func (mr *MockKeyProviderMockRecorder) KeySet(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockKeyProvider)(nil).GetKeySet), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockKeyProvider)(nil).KeySet), arg0)
}

View file

@ -1,56 +1,69 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Signer)
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: SigningKey,Key)
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
jose "gopkg.in/square/go-jose.v2"
)
// MockSigner is a mock of Signer interface.
type MockSigner struct {
// MockSigningKey is a mock of SigningKey interface.
type MockSigningKey struct {
ctrl *gomock.Controller
recorder *MockSignerMockRecorder
recorder *MockSigningKeyMockRecorder
}
// MockSignerMockRecorder is the mock recorder for MockSigner.
type MockSignerMockRecorder struct {
mock *MockSigner
// MockSigningKeyMockRecorder is the mock recorder for MockSigningKey.
type MockSigningKeyMockRecorder struct {
mock *MockSigningKey
}
// NewMockSigner creates a new mock instance.
func NewMockSigner(ctrl *gomock.Controller) *MockSigner {
mock := &MockSigner{ctrl: ctrl}
mock.recorder = &MockSignerMockRecorder{mock}
// NewMockSigningKey creates a new mock instance.
func NewMockSigningKey(ctrl *gomock.Controller) *MockSigningKey {
mock := &MockSigningKey{ctrl: ctrl}
mock.recorder = &MockSigningKeyMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSigner) EXPECT() *MockSignerMockRecorder {
func (m *MockSigningKey) EXPECT() *MockSigningKeyMockRecorder {
return m.recorder
}
// Health mocks base method.
func (m *MockSigner) Health(arg0 context.Context) error {
// ID mocks base method.
func (m *MockSigningKey) ID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health", arg0)
ret0, _ := ret[0].(error)
ret := m.ctrl.Call(m, "ID")
ret0, _ := ret[0].(string)
return ret0
}
// Health indicates an expected call of Health.
func (mr *MockSignerMockRecorder) Health(arg0 interface{}) *gomock.Call {
// ID indicates an expected call of ID.
func (mr *MockSigningKeyMockRecorder) ID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockSigner)(nil).Health), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockSigningKey)(nil).ID))
}
// Key mocks base method.
func (m *MockSigningKey) Key() interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Key")
ret0, _ := ret[0].(interface{})
return ret0
}
// Key indicates an expected call of Key.
func (mr *MockSigningKeyMockRecorder) Key() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockSigningKey)(nil).Key))
}
// SignatureAlgorithm mocks base method.
func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
func (m *MockSigningKey) SignatureAlgorithm() jose.SignatureAlgorithm {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SignatureAlgorithm")
ret0, _ := ret[0].(jose.SignatureAlgorithm)
@ -58,21 +71,86 @@ func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
}
// SignatureAlgorithm indicates an expected call of SignatureAlgorithm.
func (mr *MockSignerMockRecorder) SignatureAlgorithm() *gomock.Call {
func (mr *MockSigningKeyMockRecorder) SignatureAlgorithm() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigner)(nil).SignatureAlgorithm))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigningKey)(nil).SignatureAlgorithm))
}
// Signer mocks base method.
func (m *MockSigner) Signer() jose.Signer {
// MockKey is a mock of Key interface.
type MockKey struct {
ctrl *gomock.Controller
recorder *MockKeyMockRecorder
}
// MockKeyMockRecorder is the mock recorder for MockKey.
type MockKeyMockRecorder struct {
mock *MockKey
}
// NewMockKey creates a new mock instance.
func NewMockKey(ctrl *gomock.Controller) *MockKey {
mock := &MockKey{ctrl: ctrl}
mock.recorder = &MockKeyMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockKey) EXPECT() *MockKeyMockRecorder {
return m.recorder
}
// Algorithm mocks base method.
func (m *MockKey) Algorithm() jose.SignatureAlgorithm {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Signer")
ret0, _ := ret[0].(jose.Signer)
ret := m.ctrl.Call(m, "Algorithm")
ret0, _ := ret[0].(jose.SignatureAlgorithm)
return ret0
}
// Signer indicates an expected call of Signer.
func (mr *MockSignerMockRecorder) Signer() *gomock.Call {
// Algorithm indicates an expected call of Algorithm.
func (mr *MockKeyMockRecorder) Algorithm() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockSigner)(nil).Signer))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockKey)(nil).Algorithm))
}
// ID mocks base method.
func (m *MockKey) ID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ID")
ret0, _ := ret[0].(string)
return ret0
}
// ID indicates an expected call of ID.
func (mr *MockKeyMockRecorder) ID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockKey)(nil).ID))
}
// Key mocks base method.
func (m *MockKey) Key() interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Key")
ret0, _ := ret[0].(interface{})
return ret0
}
// Key indicates an expected call of Key.
func (mr *MockKeyMockRecorder) Key() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockKey)(nil).Key))
}
// Use mocks base method.
func (m *MockKey) Use() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Use")
ret0, _ := ret[0].(string)
return ret0
}
// Use indicates an expected call of Use.
func (mr *MockKeyMockRecorder) Use() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Use", reflect.TypeOf((*MockKey)(nil).Use))
}

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/pkg/op (interfaces: Storage)
// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Storage)
// Package mock is a generated GoMock package.
package mock
@ -10,8 +10,8 @@ import (
time "time"
gomock "github.com/golang/mock/gomock"
oidc "github.com/zitadel/oidc/pkg/oidc"
op "github.com/zitadel/oidc/pkg/op"
oidc "github.com/zitadel/oidc/v2/pkg/oidc"
op "github.com/zitadel/oidc/v2/pkg/op"
jose "gopkg.in/square/go-jose.v2"
)
@ -159,34 +159,19 @@ func (mr *MockStorageMockRecorder) GetClientByClientID(arg0, arg1 interface{}) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientByClientID", reflect.TypeOf((*MockStorage)(nil).GetClientByClientID), arg0, arg1)
}
// GetKeyByIDAndUserID mocks base method.
func (m *MockStorage) GetKeyByIDAndUserID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) {
// GetKeyByIDAndClientID mocks base method.
func (m *MockStorage) GetKeyByIDAndClientID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetKeyByIDAndUserID", arg0, arg1, arg2)
ret := m.ctrl.Call(m, "GetKeyByIDAndClientID", arg0, arg1, arg2)
ret0, _ := ret[0].(*jose.JSONWebKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetKeyByIDAndUserID indicates an expected call of GetKeyByIDAndUserID.
func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interface{}) *gomock.Call {
// GetKeyByIDAndClientID indicates an expected call of GetKeyByIDAndClientID.
func (mr *MockStorageMockRecorder) GetKeyByIDAndClientID(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2)
}
// GetKeySet mocks base method.
func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetKeySet", arg0)
ret0, _ := ret[0].(*jose.JSONWebKeySet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetKeySet indicates an expected call of GetKeySet.
func (mr *MockStorageMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockStorage)(nil).GetKeySet), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndClientID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndClientID), arg0, arg1, arg2)
}
// GetPrivateClaimsFromScopes mocks base method.
@ -204,16 +189,20 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3)
}
// GetSigningKey mocks base method.
func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey) {
// GetRefreshTokenInfo mocks base method.
func (m *MockStorage) GetRefreshTokenInfo(arg0 context.Context, arg1, arg2 string) (string, string, error) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "GetSigningKey", arg0, arg1)
ret := m.ctrl.Call(m, "GetRefreshTokenInfo", arg0, arg1, arg2)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetSigningKey indicates an expected call of GetSigningKey.
func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call {
// GetRefreshTokenInfo indicates an expected call of GetRefreshTokenInfo.
func (mr *MockStorageMockRecorder) GetRefreshTokenInfo(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRefreshTokenInfo", reflect.TypeOf((*MockStorage)(nil).GetRefreshTokenInfo), arg0, arg1, arg2)
}
// Health mocks base method.
@ -230,6 +219,21 @@ func (mr *MockStorageMockRecorder) Health(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0)
}
// KeySet mocks base method.
func (m *MockStorage) KeySet(arg0 context.Context) ([]op.Key, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeySet", arg0)
ret0, _ := ret[0].([]op.Key)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// KeySet indicates an expected call of KeySet.
func (mr *MockStorageMockRecorder) KeySet(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockStorage)(nil).KeySet), arg0)
}
// RevokeToken mocks base method.
func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error {
m.ctrl.T.Helper()
@ -259,7 +263,7 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g
}
// SetIntrospectionFromToken mocks base method.
func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error {
func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 *oidc.IntrospectionResponse, arg2, arg3, arg4 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(error)
@ -273,7 +277,7 @@ func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, a
}
// SetUserinfoFromScopes mocks base method.
func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error {
func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3 string, arg4 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(error)
@ -287,7 +291,7 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3,
}
// SetUserinfoFromToken mocks base method.
func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error {
func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3, arg4 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(error)
@ -300,6 +304,36 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4)
}
// SignatureAlgorithms mocks base method.
func (m *MockStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0)
ret0, _ := ret[0].([]jose.SignatureAlgorithm)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SignatureAlgorithms indicates an expected call of SignatureAlgorithms.
func (mr *MockStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockStorage)(nil).SignatureAlgorithms), arg0)
}
// SigningKey mocks base method.
func (m *MockStorage) SigningKey(arg0 context.Context) (op.SigningKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SigningKey", arg0)
ret0, _ := ret[0].(op.SigningKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SigningKey indicates an expected call of SigningKey.
func (mr *MockStorageMockRecorder) SigningKey(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SigningKey", reflect.TypeOf((*MockStorage)(nil).SigningKey), arg0)
}
// TerminateSession mocks base method.
func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()

View file

@ -6,13 +6,10 @@ import (
"testing"
"time"
"github.com/zitadel/oidc/pkg/oidc"
"gopkg.in/square/go-jose.v2"
"github.com/golang/mock/gomock"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
)
func NewStorage(t *testing.T) op.Storage {
@ -41,13 +38,13 @@ func NewMockStorageAny(t *testing.T) op.Storage {
func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage {
m := NewStorage(t)
ExpectSigningKeyInvalid(m)
//ExpectSigningKeyInvalid(m)
return m
}
func NewMockStorageSigningKey(t *testing.T) op.Storage {
m := NewStorage(t)
ExpectSigningKey(m)
//ExpectSigningKey(m)
return m
}
@ -85,24 +82,6 @@ func ExpectValidClientID(s op.Storage) {
})
}
func ExpectSigningKeyInvalid(s op.Storage) {
mockS := s.(*MockStorage)
mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, keyCh chan<- jose.SigningKey) {
keyCh <- jose.SigningKey{}
},
)
}
func ExpectSigningKey(s op.Storage) {
mockS := s.(*MockStorage)
mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, keyCh chan<- jose.SigningKey) {
keyCh <- jose.SigningKey{Algorithm: jose.HS256, Key: []byte("key")}
},
)
}
type ConfClient struct {
id string
appType op.ApplicationType

View file

@ -12,8 +12,8 @@ import (
"golang.org/x/text/language"
"gopkg.in/square/go-jose.v2"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
const (
@ -27,80 +27,84 @@ const (
defaultRevocationEndpoint = "revoke"
defaultEndSessionEndpoint = "end_session"
defaultKeysEndpoint = "keys"
defaultDeviceAuthzEndpoint = "/device_authorization"
)
var DefaultEndpoints = &endpoints{
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
Token: NewEndpoint(defaultTokenEndpoint),
Introspection: NewEndpoint(defaultIntrospectEndpoint),
Userinfo: NewEndpoint(defaultUserinfoEndpoint),
Revocation: NewEndpoint(defaultRevocationEndpoint),
EndSession: NewEndpoint(defaultEndSessionEndpoint),
JwksURI: NewEndpoint(defaultKeysEndpoint),
}
var (
DefaultEndpoints = &endpoints{
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
Token: NewEndpoint(defaultTokenEndpoint),
Introspection: NewEndpoint(defaultIntrospectEndpoint),
Userinfo: NewEndpoint(defaultUserinfoEndpoint),
Revocation: NewEndpoint(defaultRevocationEndpoint),
EndSession: NewEndpoint(defaultEndSessionEndpoint),
JwksURI: NewEndpoint(defaultKeysEndpoint),
DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint),
}
defaultCORSOptions = cors.Options{
AllowCredentials: true,
AllowedHeaders: []string{
"Origin",
"Accept",
"Accept-Language",
"Authorization",
"Content-Type",
"X-Requested-With",
},
AllowedMethods: []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
},
ExposedHeaders: []string{
"Location",
"Content-Length",
},
AllowOriginFunc: func(_ string) bool {
return true
},
}
)
type OpenIDProvider interface {
Configuration
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
IDTokenHintVerifier() IDTokenHintVerifier
AccessTokenVerifier() AccessTokenVerifier
IDTokenHintVerifier(context.Context) IDTokenHintVerifier
AccessTokenVerifier(context.Context) AccessTokenVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
Signer() Signer
Probes() []ProbesFn
HttpHandler() http.Handler
}
type HttpInterceptor func(http.Handler) http.Handler
var defaultCORSOptions = cors.Options{
AllowCredentials: true,
AllowedHeaders: []string{
"Origin",
"Accept",
"Accept-Language",
"Authorization",
"Content-Type",
"X-Requested-With",
},
AllowedMethods: []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
},
ExposedHeaders: []string{
"Location",
"Content-Length",
},
AllowOriginFunc: func(_ string) bool {
return true
},
}
func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router {
intercept := buildInterceptor(interceptors...)
router := mux.NewRouter()
router.Use(cors.New(defaultCORSOptions).Handler)
router.Use(intercept(o.IssuerFromRequest, interceptors...))
router.HandleFunc(healthEndpoint, healthHandler)
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer()))
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o)))
router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o)))
router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o)))
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage()))
router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o))
router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").HandlerFunc(authorizeCallbackHandler(o))
router.HandleFunc(o.TokenEndpoint().Relative(), tokenHandler(o))
router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(o))
router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o)))
router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o))
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage()))
router.HandleFunc(o.DeviceAuthorizationEndpoint().Relative(), DeviceAuthorizationHandler(o))
return router
}
// AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login
func AuthCallbackURL(o OpenIDProvider) func(string) string {
return func(requestID string) string {
return o.AuthorizationEndpoint().Absolute(o.Issuer()) + authCallbackPathSuffix + "?id=" + requestID
func AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string {
return func(ctx context.Context, requestID string) string {
return o.AuthorizationEndpoint().Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID
}
}
@ -109,7 +113,6 @@ func authCallbackPath(o OpenIDProvider) string {
}
type Config struct {
Issuer string
CryptoKey [32]byte
DefaultLogoutRedirectURI string
CodeMethodS256 bool
@ -118,44 +121,52 @@ type Config struct {
GrantTypeRefreshToken bool
RequestObjectSupported bool
SupportedUILocales []language.Tag
DeviceAuthorization DeviceAuthorizationConfig
}
type endpoints struct {
Authorization Endpoint
Token Endpoint
Introspection Endpoint
Userinfo Endpoint
Revocation Endpoint
EndSession Endpoint
CheckSessionIframe Endpoint
JwksURI Endpoint
Authorization Endpoint
Token Endpoint
Introspection Endpoint
Userinfo Endpoint
Revocation Endpoint
EndSession Endpoint
CheckSessionIframe Endpoint
JwksURI Endpoint
DeviceAuthorization Endpoint
}
// NewOpenIDProvider creates a provider. The provider provides (with HttpHandler())
// a http.Router that handles a suite of endpoints (some paths can be overridden):
// /healthz
// /ready
// /.well-known/openid-configuration
// /oauth/token
// /oauth/introspect
// /callback
// /authorize
// /userinfo
// /revoke
// /end_session
// /keys
//
// /healthz
// /ready
// /.well-known/openid-configuration
// /oauth/token
// /oauth/introspect
// /callback
// /authorize
// /userinfo
// /revoke
// /end_session
// /keys
// /device_authorization
//
// This does not include login. Login is handled with a redirect that includes the
// request ID. The redirect for logins is specified per-client by Client.LoginURL().
// Successful logins should mark the request as authorized and redirect back to to
// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back
// to the AuthCallbackURL, the request id should be passed as the "id" parameter.
func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) {
err := ValidateIssuer(config.Issuer)
if err != nil {
return nil, err
}
func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return newProvider(config, storage, StaticIssuer(issuer), opOpts...)
}
o := &openidProvider{
func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return newProvider(config, storage, IssuerFromHost(path), opOpts...)
}
func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) {
o := &Provider{
config: config,
storage: storage,
endpoints: DefaultEndpoints,
@ -168,36 +179,32 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO
}
}
keyCh := make(chan jose.SigningKey)
go storage.GetSigningKey(ctx, keyCh)
o.signer = NewSigner(ctx, storage, keyCh)
o.issuer, err = issuer(o.insecure)
if err != nil {
return nil, err
}
o.httpHandler = CreateRouter(o, o.interceptors...)
o.decoder = schema.NewDecoder()
o.decoder.IgnoreUnknownKeys(true)
o.encoder = schema.NewEncoder()
o.encoder = oidc.NewEncoder()
o.crypto = NewAESCrypto(config.CryptoKey)
// Avoid potential race conditions by calling these early
_ = o.AccessTokenVerifier() // sets accessTokenVerifier
_ = o.IDTokenHintVerifier() // sets idTokenHintVerifier
_ = o.JWTProfileVerifier() // sets jwtProfileVerifier
_ = o.openIDKeySet() // sets keySet
_ = o.openIDKeySet() // sets keySet
return o, nil
}
type openidProvider struct {
type Provider struct {
config *Config
issuer IssuerFromRequest
insecure bool
endpoints *endpoints
storage Storage
signer Signer
idTokenHintVerifier IDTokenHintVerifier
jwtProfileVerifier JWTProfileVerifier
accessTokenVerifier AccessTokenVerifier
keySet *openIDKeySet
crypto Crypto
httpHandler http.Handler
@ -209,159 +216,163 @@ type openidProvider struct {
idTokenHintVerifierOpts []IDTokenHintVerifierOpt
}
func (o *openidProvider) Issuer() string {
return o.config.Issuer
func (o *Provider) IssuerFromRequest(r *http.Request) string {
return o.issuer(r)
}
func (o *openidProvider) AuthorizationEndpoint() Endpoint {
func (o *Provider) Insecure() bool {
return o.insecure
}
func (o *Provider) AuthorizationEndpoint() Endpoint {
return o.endpoints.Authorization
}
func (o *openidProvider) TokenEndpoint() Endpoint {
func (o *Provider) TokenEndpoint() Endpoint {
return o.endpoints.Token
}
func (o *openidProvider) IntrospectionEndpoint() Endpoint {
func (o *Provider) IntrospectionEndpoint() Endpoint {
return o.endpoints.Introspection
}
func (o *openidProvider) UserinfoEndpoint() Endpoint {
func (o *Provider) UserinfoEndpoint() Endpoint {
return o.endpoints.Userinfo
}
func (o *openidProvider) RevocationEndpoint() Endpoint {
func (o *Provider) RevocationEndpoint() Endpoint {
return o.endpoints.Revocation
}
func (o *openidProvider) EndSessionEndpoint() Endpoint {
func (o *Provider) EndSessionEndpoint() Endpoint {
return o.endpoints.EndSession
}
func (o *openidProvider) KeysEndpoint() Endpoint {
func (o *Provider) DeviceAuthorizationEndpoint() Endpoint {
return o.endpoints.DeviceAuthorization
}
func (o *Provider) KeysEndpoint() Endpoint {
return o.endpoints.JwksURI
}
func (o *openidProvider) AuthMethodPostSupported() bool {
func (o *Provider) AuthMethodPostSupported() bool {
return o.config.AuthMethodPost
}
func (o *openidProvider) CodeMethodS256Supported() bool {
func (o *Provider) CodeMethodS256Supported() bool {
return o.config.CodeMethodS256
}
func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool {
func (o *Provider) AuthMethodPrivateKeyJWTSupported() bool {
return o.config.AuthMethodPrivateKeyJWT
}
func (o *openidProvider) TokenEndpointSigningAlgorithmsSupported() []string {
func (o *Provider) TokenEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *openidProvider) GrantTypeRefreshTokenSupported() bool {
func (o *Provider) GrantTypeRefreshTokenSupported() bool {
return o.config.GrantTypeRefreshToken
}
func (o *openidProvider) GrantTypeTokenExchangeSupported() bool {
return false
func (o *Provider) GrantTypeTokenExchangeSupported() bool {
_, ok := o.storage.(TokenExchangeStorage)
return ok
}
func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool {
func (o *Provider) GrantTypeJWTAuthorizationSupported() bool {
return true
}
func (o *openidProvider) GrantTypeClientCredentialsSupported() bool {
func (o *Provider) GrantTypeDeviceCodeSupported() bool {
_, ok := o.storage.(DeviceAuthorizationStorage)
return ok
}
func (o *Provider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
return true
}
func (o *Provider) IntrospectionEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *Provider) GrantTypeClientCredentialsSupported() bool {
_, ok := o.storage.(ClientCredentialsStorage)
return ok
}
func (o *openidProvider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
func (o *Provider) RevocationAuthMethodPrivateKeyJWTSupported() bool {
return true
}
func (o *openidProvider) IntrospectionEndpointSigningAlgorithmsSupported() []string {
func (o *Provider) RevocationEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *openidProvider) RevocationAuthMethodPrivateKeyJWTSupported() bool {
return true
}
func (o *openidProvider) RevocationEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *openidProvider) RequestObjectSupported() bool {
func (o *Provider) RequestObjectSupported() bool {
return o.config.RequestObjectSupported
}
func (o *openidProvider) RequestObjectSigningAlgorithmsSupported() []string {
func (o *Provider) RequestObjectSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *openidProvider) SupportedUILocales() []language.Tag {
func (o *Provider) SupportedUILocales() []language.Tag {
return o.config.SupportedUILocales
}
func (o *openidProvider) Storage() Storage {
func (o *Provider) DeviceAuthorization() DeviceAuthorizationConfig {
return o.config.DeviceAuthorization
}
func (o *Provider) Storage() Storage {
return o.storage
}
func (o *openidProvider) Decoder() httphelper.Decoder {
func (o *Provider) Decoder() httphelper.Decoder {
return o.decoder
}
func (o *openidProvider) Encoder() httphelper.Encoder {
func (o *Provider) Encoder() httphelper.Encoder {
return o.encoder
}
func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier {
if o.idTokenHintVerifier == nil {
o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), o.openIDKeySet(), o.idTokenHintVerifierOpts...)
}
return o.idTokenHintVerifier
func (o *Provider) IDTokenHintVerifier(ctx context.Context) IDTokenHintVerifier {
return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.idTokenHintVerifierOpts...)
}
func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier {
if o.jwtProfileVerifier == nil {
o.jwtProfileVerifier = NewJWTProfileVerifier(o.Storage(), o.Issuer(), 1*time.Hour, time.Second)
}
return o.jwtProfileVerifier
func (o *Provider) JWTProfileVerifier(ctx context.Context) JWTProfileVerifier {
return NewJWTProfileVerifier(o.Storage(), IssuerFromContext(ctx), 1*time.Hour, time.Second)
}
func (o *openidProvider) AccessTokenVerifier() AccessTokenVerifier {
if o.accessTokenVerifier == nil {
o.accessTokenVerifier = NewAccessTokenVerifier(o.Issuer(), o.openIDKeySet(), o.accessTokenVerifierOpts...)
}
return o.accessTokenVerifier
func (o *Provider) AccessTokenVerifier(ctx context.Context) AccessTokenVerifier {
return NewAccessTokenVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.accessTokenVerifierOpts...)
}
func (o *openidProvider) openIDKeySet() oidc.KeySet {
func (o *Provider) openIDKeySet() oidc.KeySet {
if o.keySet == nil {
o.keySet = &openIDKeySet{o.Storage()}
}
return o.keySet
}
func (o *openidProvider) Crypto() Crypto {
func (o *Provider) Crypto() Crypto {
return o.crypto
}
func (o *openidProvider) DefaultLogoutRedirectURI() string {
func (o *Provider) DefaultLogoutRedirectURI() string {
return o.config.DefaultLogoutRedirectURI
}
func (o *openidProvider) Signer() Signer {
return o.signer
}
func (o *openidProvider) Probes() []ProbesFn {
func (o *Provider) Probes() []ProbesFn {
return []ProbesFn{
ReadySigner(o.Signer()),
ReadyStorage(o.Storage()),
}
}
func (o *openidProvider) HttpHandler() http.Handler {
func (o *Provider) HttpHandler() http.Handler {
return o.httpHandler
}
@ -372,22 +383,31 @@ type openIDKeySet struct {
// VerifySignature implements the oidc.KeySet interface
// providing an implementation for the keys stored in the OP Storage interface
func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
keySet, err := o.Storage.GetKeySet(ctx)
keySet, err := o.Storage.KeySet(ctx)
if err != nil {
return nil, fmt.Errorf("error fetching keys: %w", err)
}
keyID, alg := oidc.GetKeyIDAndAlg(jws)
key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...)
key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, jsonWebKeySet(keySet).Keys...)
if err != nil {
return nil, fmt.Errorf("invalid signature: %w", err)
}
return jws.Verify(&key)
}
type Option func(o *openidProvider) error
type Option func(o *Provider) error
// WithAllowInsecure allows the use of http (instead of https) for issuers
// this is not recommended for production use and violates the OIDC specification
func WithAllowInsecure() Option {
return func(o *Provider) error {
o.insecure = true
return nil
}
}
func WithCustomAuthEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@ -397,7 +417,7 @@ func WithCustomAuthEndpoint(endpoint Endpoint) Option {
}
func WithCustomTokenEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@ -407,7 +427,7 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option {
}
func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@ -417,7 +437,7 @@ func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option {
}
func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@ -427,7 +447,7 @@ func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
}
func WithCustomRevocationEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@ -437,7 +457,7 @@ func WithCustomRevocationEndpoint(endpoint Endpoint) Option {
}
func WithCustomEndSessionEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@ -447,7 +467,7 @@ func WithCustomEndSessionEndpoint(endpoint Endpoint) Option {
}
func WithCustomKeysEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@ -457,7 +477,7 @@ func WithCustomKeysEndpoint(endpoint Endpoint) Option {
}
func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys Endpoint) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
o.endpoints.Authorization = auth
o.endpoints.Token = token
o.endpoints.Userinfo = userInfo
@ -469,38 +489,32 @@ func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys End
}
func WithHttpInterceptors(interceptors ...HttpInterceptor) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
o.interceptors = append(o.interceptors, interceptors...)
return nil
}
}
func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
o.accessTokenVerifierOpts = opts
return nil
}
}
func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option {
return func(o *openidProvider) error {
return func(o *Provider) error {
o.idTokenHintVerifierOpts = opts
return nil
}
}
func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler {
return func(handlerFunc http.HandlerFunc) http.Handler {
handler := handlerFuncToHandler(handlerFunc)
func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handler http.Handler) http.Handler {
issuerInterceptor := NewIssuerInterceptor(i)
return func(handler http.Handler) http.Handler {
for i := len(interceptors) - 1; i >= 0; i-- {
handler = interceptors[i](handler)
}
return handler
return cors.New(defaultCORSOptions).Handler(issuerInterceptor.Handler(handler))
}
}
func handlerFuncToHandler(handlerFunc http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handlerFunc(w, r)
})
}

392
pkg/op/op_test.go Normal file
View file

@ -0,0 +1,392 @@
package op_test
import (
"context"
"crypto/sha256"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/example/server/storage"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
"golang.org/x/text/language"
)
var testProvider op.OpenIDProvider
const (
testIssuer = "https://localhost:9998/"
pathLoggedOut = "/logged-out"
)
func init() {
config := &op.Config{
CryptoKey: sha256.Sum256([]byte("test")),
DefaultLogoutRedirectURI: pathLoggedOut,
CodeMethodS256: true,
AuthMethodPost: true,
AuthMethodPrivateKeyJWT: true,
GrantTypeRefreshToken: true,
RequestObjectSupported: true,
SupportedUILocales: []language.Tag{language.English},
DeviceAuthorization: op.DeviceAuthorizationConfig{
Lifetime: 5 * time.Minute,
PollInterval: 5 * time.Second,
UserFormURL: testIssuer + "device",
UserCode: op.UserCodeBase20,
},
}
storage.RegisterClients(
storage.NativeClient("native"),
storage.WebClient("web", "secret", "https://example.com"),
storage.WebClient("api", "secret"),
)
var err error
testProvider, err = op.NewOpenIDProvider(testIssuer, config,
storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(),
)
if err != nil {
panic(err)
}
}
type routesTestStorage interface {
op.Storage
AuthRequestDone(id string) error
}
func mapAsValues(m map[string]string) string {
values := make(url.Values, len(m))
for k, v := range m {
values.Set(k, v)
}
return values.Encode()
}
func TestRoutes(t *testing.T) {
storage := testProvider.Storage().(routesTestStorage)
ctx := op.ContextWithIssuer(context.Background(), testIssuer)
client, err := storage.GetClientByClientID(ctx, "web")
require.NoError(t, err)
oidcAuthReq := &oidc.AuthRequest{
ClientID: client.GetID(),
RedirectURI: "https://example.com",
MaxAge: gu.Ptr[uint](300),
Scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopePhone},
ResponseType: oidc.ResponseTypeCode,
}
authReq, err := storage.CreateAuthRequest(ctx, oidcAuthReq, "id1")
require.NoError(t, err)
storage.AuthRequestDone(authReq.GetID())
accessToken, refreshToken, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "")
require.NoError(t, err)
accessTokenRevoke, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "")
require.NoError(t, err)
idToken, err := op.CreateIDToken(ctx, testIssuer, authReq, time.Hour, accessToken, "123", storage, client)
require.NoError(t, err)
jwtToken, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeJWT, testProvider, client, "")
require.NoError(t, err)
oidcAuthReq.IDTokenHint = idToken
serverURL, err := url.Parse(testIssuer)
require.NoError(t, err)
type basicAuth struct {
username, password string
}
tests := []struct {
name string
method string
path string
basicAuth *basicAuth
header map[string]string
values map[string]string
body map[string]string
wantCode int
headerContains map[string]string
json string // test for exact json output
contains []string // when the body output is not constant, we just check for snippets to be present in the response
}{
{
name: "health",
method: http.MethodGet,
path: "/healthz",
wantCode: http.StatusOK,
json: `{"status":"ok"}`,
},
{
name: "ready",
method: http.MethodGet,
path: "/ready",
wantCode: http.StatusOK,
json: `{"status":"ok"}`,
},
{
name: "discovery",
method: http.MethodGet,
path: oidc.DiscoveryEndpoint,
wantCode: http.StatusOK,
json: `{"issuer":"https://localhost:9998/","authorization_endpoint":"https://localhost:9998/authorize","token_endpoint":"https://localhost:9998/oauth/token","introspection_endpoint":"https://localhost:9998/oauth/introspect","userinfo_endpoint":"https://localhost:9998/userinfo","revocation_endpoint":"https://localhost:9998/revoke","end_session_endpoint":"https://localhost:9998/end_session","device_authorization_endpoint":"https://localhost:9998/device_authorization","jwks_uri":"https://localhost:9998/keys","scopes_supported":["openid","profile","email","phone","address","offline_access"],"response_types_supported":["code","id_token","id_token token"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials","urn:ietf:params:oauth:grant-type:token-exchange","urn:ietf:params:oauth:grant-type:jwt-bearer","urn:ietf:params:oauth:grant-type:device_code"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"request_object_signing_alg_values_supported":["RS256"],"token_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"revocation_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["RS256"],"introspection_endpoint_auth_methods_supported":["client_secret_basic","private_key_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["RS256"],"claims_supported":["sub","aud","exp","iat","iss","auth_time","nonce","acr","amr","c_hash","at_hash","act","scopes","client_id","azp","preferred_username","name","family_name","given_name","locale","email","email_verified","phone_number","phone_number_verified"],"code_challenge_methods_supported":["S256"],"ui_locales_supported":["en"],"request_parameter_supported":true,"request_uri_parameter_supported":false}`,
},
{
name: "authorization",
method: http.MethodGet,
path: testProvider.AuthorizationEndpoint().Relative(),
values: map[string]string{
"client_id": client.GetID(),
"redirect_uri": "https://example.com",
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
"response_type": string(oidc.ResponseTypeCode),
},
wantCode: http.StatusFound,
headerContains: map[string]string{"Location": "/login/username?authRequestID="},
},
{
name: "authorization callback",
method: http.MethodGet,
path: testProvider.AuthorizationEndpoint().Relative() + "/callback",
values: map[string]string{"id": authReq.GetID()},
wantCode: http.StatusFound,
headerContains: map[string]string{"Location": "https://example.com?code="},
contains: []string{
`<a href="https://example.com?code=`,
">Found</a>.",
},
},
{
// This call will fail. A successfull test is already
// part of client/integration_test.go
name: "code exchange",
method: http.MethodGet,
path: testProvider.TokenEndpoint().Relative(),
values: map[string]string{
"grant_type": string(oidc.GrantTypeCode),
"code": "123",
},
wantCode: http.StatusUnauthorized,
json: `{"error":"invalid_client"}`,
},
{
name: "JWT authorization",
method: http.MethodGet,
path: testProvider.TokenEndpoint().Relative(),
values: map[string]string{
"grant_type": string(oidc.GrantTypeBearer),
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
"assertion": jwtToken,
},
wantCode: http.StatusBadRequest,
json: "{\"error\":\"server_error\",\"error_description\":\"audience is not valid: Audience must contain client_id \\\"https://localhost:9998/\\\"\"}",
},
{
name: "Token exchange",
method: http.MethodGet,
path: testProvider.TokenEndpoint().Relative(),
basicAuth: &basicAuth{"web", "secret"},
values: map[string]string{
"grant_type": string(oidc.GrantTypeTokenExchange),
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
"subject_token": jwtToken,
"subject_token_type": string(oidc.AccessTokenType),
},
wantCode: http.StatusOK,
contains: []string{
`{"access_token":"`,
`","issued_token_type":"urn:ietf:params:oauth:token-type:refresh_token","token_type":"Bearer","expires_in":299,"scope":"openid offline_access","refresh_token":"`,
},
},
{
name: "Client credentials exchange",
method: http.MethodGet,
path: testProvider.TokenEndpoint().Relative(),
basicAuth: &basicAuth{"sid1", "verysecret"},
values: map[string]string{
"grant_type": string(oidc.GrantTypeClientCredentials),
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
},
wantCode: http.StatusOK,
contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`},
},
{
// This call will fail. A successfull test is already
// part of device_test.go
name: "device token",
method: http.MethodPost,
path: testProvider.TokenEndpoint().Relative(),
basicAuth: &basicAuth{"web", "secret"},
header: map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
},
body: map[string]string{
"grant_type": string(oidc.GrantTypeDeviceCode),
"device_code": "123",
},
wantCode: http.StatusBadRequest,
json: `{"error":"access_denied","error_description":"The authorization request was denied."}`,
},
{
name: "missing grant type",
method: http.MethodGet,
path: testProvider.TokenEndpoint().Relative(),
wantCode: http.StatusBadRequest,
json: `{"error":"invalid_request","error_description":"grant_type missing"}`,
},
{
name: "unsupported grant type",
method: http.MethodGet,
path: testProvider.TokenEndpoint().Relative(),
values: map[string]string{
"grant_type": "foo",
},
wantCode: http.StatusBadRequest,
json: `{"error":"unsupported_grant_type","error_description":"foo not supported"}`,
},
{
name: "introspection",
method: http.MethodGet,
path: testProvider.IntrospectionEndpoint().Relative(),
basicAuth: &basicAuth{"web", "secret"},
values: map[string]string{
"token": accessToken,
},
wantCode: http.StatusOK,
json: `{"active":true,"scope":"openid offline_access email profile phone","client_id":"web","sub":"id1","username":"test-user@localhost","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`,
},
{
name: "user info",
method: http.MethodGet,
path: testProvider.UserinfoEndpoint().Relative(),
header: map[string]string{
"authorization": "Bearer " + accessToken,
},
wantCode: http.StatusOK,
json: `{"sub":"id1","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`,
},
{
name: "refresh token",
method: http.MethodGet,
path: testProvider.TokenEndpoint().Relative(),
values: map[string]string{
"grant_type": string(oidc.GrantTypeRefreshToken),
"refresh_token": refreshToken,
"client_id": client.GetID(),
"client_secret": "secret",
},
wantCode: http.StatusOK,
contains: []string{
`{"access_token":"`,
`","token_type":"Bearer","refresh_token":"`,
`","expires_in":299,"id_token":"`,
},
},
{
name: "revoke",
method: http.MethodGet,
path: testProvider.RevocationEndpoint().Relative(),
basicAuth: &basicAuth{"web", "secret"},
values: map[string]string{
"token": accessTokenRevoke,
},
wantCode: http.StatusOK,
},
{
name: "end session",
method: http.MethodGet,
path: testProvider.EndSessionEndpoint().Relative(),
values: map[string]string{
"id_token_hint": idToken,
"client_id": "web",
},
wantCode: http.StatusFound,
headerContains: map[string]string{"Location": "/logged-out"},
contains: []string{`<a href="/logged-out">Found</a>.`},
},
{
name: "keys",
method: http.MethodGet,
path: testProvider.KeysEndpoint().Relative(),
wantCode: http.StatusOK,
contains: []string{
`{"keys":[{"use":"sig","kty":"RSA","kid":"`,
`","alg":"RS256","n":"`, `","e":"AQAB"}]}`,
},
},
{
name: "device authorization",
method: http.MethodGet,
path: testProvider.DeviceAuthorizationEndpoint().Relative(),
basicAuth: &basicAuth{"web", "secret"},
values: map[string]string{
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
},
wantCode: http.StatusOK,
contains: []string{
`{"device_code":"`, `","user_code":"`,
`","verification_uri":"https://localhost:9998/device"`,
`"verification_uri_complete":"https://localhost:9998/device?user_code=`,
`","expires_in":300,"interval":5}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := gu.PtrCopy(serverURL)
u.Path = tt.path
if tt.values != nil {
u.RawQuery = mapAsValues(tt.values)
}
var body io.Reader
if tt.body != nil {
body = strings.NewReader(mapAsValues(tt.body))
}
req := httptest.NewRequest(tt.method, u.String(), body)
for k, v := range tt.header {
req.Header.Set(k, v)
}
if tt.basicAuth != nil {
req.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password)
}
rec := httptest.NewRecorder()
testProvider.HttpHandler().ServeHTTP(rec, req)
resp := rec.Result()
require.NoError(t, err)
assert.Equal(t, tt.wantCode, resp.StatusCode)
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
respBodyString := string(respBody)
t.Log(respBodyString)
t.Log(resp.Header)
if tt.json != "" {
assert.JSONEq(t, tt.json, respBodyString)
}
for _, c := range tt.contains {
assert.Contains(t, respBodyString, c)
}
for k, v := range tt.headerContains {
assert.Contains(t, resp.Header.Get(k), v)
}
})
}
}

View file

@ -5,7 +5,7 @@ import (
"errors"
"net/http"
httphelper "github.com/zitadel/oidc/pkg/http"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
)
type ProbesFn func(context.Context) error
@ -31,15 +31,6 @@ func Readiness(w http.ResponseWriter, r *http.Request, probes ...ProbesFn) {
ok(w)
}
func ReadySigner(s Signer) ProbesFn {
return func(ctx context.Context) error {
if s == nil {
return errors.New("no signer")
}
return s.Health(ctx)
}
}
func ReadyStorage(s Storage) ProbesFn {
return func(ctx context.Context) error {
if s == nil {

View file

@ -6,14 +6,14 @@ import (
"net/url"
"path"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type SessionEnder interface {
Decoder() httphelper.Decoder
Storage() Storage
IDTokenHintVerifier() IDTokenHintVerifier
IDTokenHintVerifier(context.Context) IDTokenHintVerifier
DefaultLogoutRedirectURI() string
}
@ -60,7 +60,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
RedirectURI: ender.DefaultLogoutRedirectURI(),
}
if req.IdTokenHint != "" {
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier())
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx))
if err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err)
}

View file

@ -1,88 +1,38 @@
package op
import (
"context"
"errors"
"sync"
"github.com/zitadel/logging"
"gopkg.in/square/go-jose.v2"
)
type Signer interface {
Health(ctx context.Context) error
Signer() jose.Signer
var (
ErrSignerCreationFailed = errors.New("signer creation failed")
)
type SigningKey interface {
SignatureAlgorithm() jose.SignatureAlgorithm
Key() interface{}
ID() string
}
type tokenSigner struct {
signer jose.Signer
storage AuthStorage
alg jose.SignatureAlgorithm
lock sync.RWMutex
}
func NewSigner(ctx context.Context, storage AuthStorage, keyCh <-chan jose.SigningKey) Signer {
s := &tokenSigner{
storage: storage,
}
select {
case <-ctx.Done():
return nil
case key := <-keyCh:
s.exchangeSigningKey(key)
}
go s.refreshSigningKey(ctx, keyCh)
return s
}
func (s *tokenSigner) Health(_ context.Context) error {
if s.signer == nil {
return errors.New("no signer")
}
if string(s.alg) == "" {
return errors.New("no signing algorithm")
}
return nil
}
func (s *tokenSigner) Signer() jose.Signer {
s.lock.RLock()
defer s.lock.RUnlock()
return s.signer
}
func (s *tokenSigner) refreshSigningKey(ctx context.Context, keyCh <-chan jose.SigningKey) {
for {
select {
case <-ctx.Done():
return
case key := <-keyCh:
s.exchangeSigningKey(key)
}
}
}
func (s *tokenSigner) exchangeSigningKey(key jose.SigningKey) {
s.lock.Lock()
defer s.lock.Unlock()
s.alg = key.Algorithm
if key.Algorithm == "" || key.Key == nil {
s.signer = nil
logging.Warn("signer has no key")
return
}
var err error
s.signer, err = jose.NewSigner(key, &jose.SignerOptions{})
func SignerFromKey(key SigningKey) (jose.Signer, error) {
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: key.SignatureAlgorithm(),
Key: &jose.JSONWebKey{
Key: key.Key(),
KeyID: key.ID(),
},
}, &jose.SignerOptions{})
if err != nil {
logging.New().WithError(err).Error("error creating signer")
return
return nil, ErrSignerCreationFailed //TODO: log / wrap error?
}
logging.Info("signer exchanged signing key")
return signer, nil
}
func (s *tokenSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
return s.alg
type Key interface {
ID() string
Algorithm() jose.SignatureAlgorithm
Use() string
Key() interface{}
}

View file

@ -7,7 +7,7 @@ import (
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type AuthStorage interface {
@ -25,6 +25,8 @@ type AuthStorage interface {
//
// * *oidc.JWTTokenRequest from a JWT that is the assertion value of a JWT Profile
// Grant: https://datatracker.ietf.org/doc/html/rfc7523#section-2.1
//
// * TokenExchangeRequest as returned by ValidateTokenExchangeRequest
CreateAccessToken(context.Context, TokenRequest) (accessTokenID string, expiration time.Time, err error)
// The TokenRequest parameter of CreateAccessAndRefreshTokens can be any of:
@ -36,6 +38,8 @@ type AuthStorage interface {
// * AuthRequest as by returned by the AuthRequestByID or AuthRequestByCode (above).
// Used for the authorization code flow which requested offline_access scope and
// registered the refresh_token grant type in advance
//
// * TokenExchangeRequest as returned by ValidateTokenExchangeRequest
CreateAccessAndRefreshTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshTokenID string, expiration time.Time, err error)
TokenRequestByRefreshToken(ctx context.Context, refreshTokenID string) (RefreshTokenRequest, error)
@ -44,44 +48,85 @@ type AuthStorage interface {
// RevokeToken should revoke a token. In the situation that the original request was to
// revoke an access token, then tokenOrTokenID will be a tokenID and userID will be set
// but if the original request was for a refresh token, then userID will be empty and
// tokenOrTokenID will be the refresh token, not its ID.
// tokenOrTokenID will be the refresh token, not its ID. RevokeToken depends upon GetRefreshTokenInfo
// to get information from refresh tokens that are not either "<tokenID>:<userID>" strings
// nor JWTs.
RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error
GetSigningKey(context.Context, chan<- jose.SigningKey)
GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
}
// CanRefreshTokenInfo is an optional additional interface that Storage can support.
// Supporting CanRefreshTokenInfo is required to be able to (revoke) a refresh token that
// is neither an encrypted string of <tokenID>:<userID> nor a JWT.
type CanRefreshTokenInfo interface {
// GetRefreshTokenInfo must return ErrInvalidRefreshToken when presented
// with a token that is not a refresh token.
GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error)
SigningKey(context.Context) (SigningKey, error)
SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error)
KeySet(context.Context) ([]Key, error)
}
type ClientCredentialsStorage interface {
ClientCredentials(ctx context.Context, clientID, clientSecret string) (Client, error)
ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
}
type TokenExchangeStorage interface {
// ValidateTokenExchangeRequest will be called to validate parsed (including tokens) Token Exchange Grant request.
//
// Important validations can include:
// - permissions
// - set requested token type to some default value if it is empty (rfc 8693 allows it) using SetRequestedTokenType method.
// Depending on RequestedTokenType - the following tokens will be issued:
// - RefreshTokenType - both access and refresh tokens
// - AccessTokenType - only access token
// - IDTokenType - only id token
// - validation of subject's token type on possibility to be exchanged to the requested token type (according to your requirements)
// - scopes (and update them using SetCurrentScopes method)
// - set new subject if it differs from exchange subject (impersonation flow)
//
// Request will include subject's and/or actor's token claims if correspinding tokens are access/id_token issued by op
// or third party tokens parsed by TokenExchangeTokensVerifierStorage interface methods.
ValidateTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) error
// CreateTokenExchangeRequest will be called after parsing and validating token exchange request.
// Stored request is not accessed later by op - so it is up to implementer to decide
// should this method actually store the request or not (common use case - store for it for audit purposes)
CreateTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) error
// GetPrivateClaimsFromTokenExchangeRequest will be called during access token creation.
// Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc.
GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) (claims map[string]interface{}, err error)
// SetUserinfoFromTokenExchangeRequest will be called during id token creation.
// Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc.
SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request TokenExchangeRequest) error
}
// TokenExchangeTokensVerifierStorage is an optional interface used in token exchange process to verify tokens
// issued by third-party applications. If interface is not implemented - only tokens issued by op will be exchanged.
type TokenExchangeTokensVerifierStorage interface {
VerifyExchangeSubjectToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, subject string, tokenClaims map[string]interface{}, err error)
VerifyExchangeActorToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, actor string, tokenClaims map[string]interface{}, err error)
}
var ErrInvalidRefreshToken = errors.New("invalid_refresh_token")
type ClientCredentialsStorage interface {
ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
}
type OPStorage interface {
// GetClientByClientID loads a Client. The returned Client is never cached and is only used to
// handle the current request.
GetClientByClientID(ctx context.Context, clientID string) (Client, error)
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error
SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error
SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error
SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error
SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error
SetIntrospectionFromToken(ctx context.Context, userinfo *oidc.IntrospectionResponse, tokenID, subject, clientID string) error
GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error)
// GetKeyByIDAndUserID is mis-named. It does not pass userID. Instead
// it passes the clientID.
GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error)
GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error)
ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error)
}
// JWTProfileTokenStorage is an additional, optional storage to implement
// implementing it, allows specifying the [AccessTokenType] of the access_token returned form the JWT Profile TokenRequest
type JWTProfileTokenStorage interface {
JWTProfileTokenType(ctx context.Context, request TokenRequest) (AccessTokenType, error)
}
// Storage is a required parameter for NewOpenIDProvider(). In addition to the
// embedded interfaces below, if the passed Storage implements ClientCredentialsStorage
// then the grant type "client_credentials" will be supported. In that case, the access
@ -102,3 +147,50 @@ type EndSessionRequest struct {
ClientID string
RedirectURI string
}
var ErrDuplicateUserCode = errors.New("user code already exists")
type DeviceAuthorizationState struct {
ClientID string
Scopes []string
Expires time.Time
Done bool
Subject string
Denied bool
}
type DeviceAuthorizationStorage interface {
// StoreDeviceAuthorizationRequest stores a new device authorization request in the database.
// User code will be used by the user to complete the login flow and must be unique.
// ErrDuplicateUserCode signals the caller should try again with a new code.
//
// Note that user codes are low entropy keys and when many exist in the
// database, the change for collisions increases. Therefore implementers
// of this interface must make sure that user codes of expired authentication flows are purged,
// after some time.
StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error
// GetDeviceAuthorizatonState returns the current state of the device authorization flow in the database.
// The method is polled untill the the authorization is eighter Completed, Expired or Denied.
GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*DeviceAuthorizationState, error)
// GetDeviceAuthorizationByUserCode resturn the current state of the device authorization flow,
// identified by the user code.
GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*DeviceAuthorizationState, error)
// CompleteDeviceAuthorization marks a device authorization entry as Completed,
// identified by userCode. The Subject is added to the state, so that
// GetDeviceAuthorizatonState can use it to create a new Access Token.
CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error
// DenyDeviceAuthorization marks a device authorization entry as Denied.
DenyDeviceAuthorization(ctx context.Context, userCode string) error
}
func assertDeviceStorage(s Storage) (DeviceAuthorizationStorage, error) {
storage, ok := s.(DeviceAuthorizationStorage)
if !ok {
return nil, oidc.ErrUnsupportedGrantType().WithDescription("device_code grant not supported")
}
return storage, nil
}

View file

@ -4,14 +4,12 @@ import (
"context"
"time"
"github.com/zitadel/oidc/pkg/crypto"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/strings"
"github.com/zitadel/oidc/v2/pkg/crypto"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/strings"
)
type TokenCreator interface {
Issuer() string
Signer() Signer
Storage() Storage
Crypto() Crypto
}
@ -22,6 +20,13 @@ type TokenRequest interface {
GetScopes() []string
}
type AccessTokenClient interface {
GetID() string
ClockSkew() time.Duration
RestrictAdditionalAccessTokenScopes() func(scopes []string) []string
GrantTypes() []oidc.GrantType
}
func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code, refreshToken string) (*oidc.AccessTokenResponse, error) {
var accessToken, newRefreshToken string
var validity time.Duration
@ -32,7 +37,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
return nil, err
}
}
idToken, err := CreateIDToken(ctx, creator.Issuer(), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client)
idToken, err := CreateIDToken(ctx, IssuerFromContext(ctx), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), client)
if err != nil {
return nil, err
}
@ -57,7 +62,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
}, nil
}
func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client Client) (id, newRefreshToken string, exp time.Time, err error) {
func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client AccessTokenClient) (id, newRefreshToken string, exp time.Time, err error) {
if needsRefreshToken(tokenRequest, client) {
return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken)
}
@ -65,10 +70,12 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag
return
}
func needsRefreshToken(tokenRequest TokenRequest, client Client) bool {
func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool {
switch req := tokenRequest.(type) {
case AuthRequest:
return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken)
case TokenExchangeRequest:
return req.GetRequestedTokenType() == oidc.RefreshTokenType
case RefreshTokenRequest:
return true
default:
@ -76,7 +83,7 @@ func needsRefreshToken(tokenRequest TokenRequest, client Client) bool {
}
}
func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) {
func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client AccessTokenClient, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) {
id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken, client)
if err != nil {
return "", "", 0, err
@ -87,7 +94,7 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok
}
validity = exp.Add(clockSkew).Sub(time.Now().UTC())
if accessTokenType == AccessTokenTypeJWT {
accessToken, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage())
accessToken, err = CreateJWT(ctx, IssuerFromContext(ctx), tokenRequest, exp, id, client, creator.Storage())
return
}
accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
@ -98,17 +105,41 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) {
return crypto.Encrypt(tokenID + ":" + subject)
}
func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer, client Client, storage Storage) (string, error) {
func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, client AccessTokenClient, storage Storage) (string, error) {
claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew())
if client != nil {
restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes())
privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes))
var (
privateClaims map[string]interface{}
err error
)
tokenExchangeRequest, okReq := tokenRequest.(TokenExchangeRequest)
teStorage, okStorage := storage.(TokenExchangeStorage)
if okReq && okStorage {
privateClaims, err = teStorage.GetPrivateClaimsFromTokenExchangeRequest(
ctx,
tokenExchangeRequest,
)
} else {
privateClaims, err = storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes))
}
if err != nil {
return "", err
}
claims.SetPrivateClaims(privateClaims)
claims.Claims = privateClaims
}
return crypto.Sign(claims, signer.Signer())
signingKey, err := storage.SigningKey(ctx)
if err != nil {
return "", err
}
signer, err := SignerFromKey(signingKey)
if err != nil {
return "", err
}
return crypto.Sign(claims, signer)
}
type IDTokenRequest interface {
@ -120,7 +151,7 @@ type IDTokenRequest interface {
GetSubject() string
}
func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) {
func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, client Client) (string, error) {
exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity)
var acr, nonce string
if authRequest, ok := request.(AuthRequest); ok {
@ -129,33 +160,50 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v
}
claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, request.GetAMR(), request.GetClientID(), client.ClockSkew())
scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes())
signingKey, err := storage.SigningKey(ctx)
if err != nil {
return "", err
}
if accessToken != "" {
atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm())
atHash, err := oidc.ClaimHash(accessToken, signingKey.SignatureAlgorithm())
if err != nil {
return "", err
}
claims.SetAccessTokenHash(atHash)
claims.AccessTokenHash = atHash
if !client.IDTokenUserinfoClaimsAssertion() {
scopes = removeUserinfoScopes(scopes)
}
}
if len(scopes) > 0 {
userInfo := oidc.NewUserInfo()
tokenExchangeRequest, okReq := request.(TokenExchangeRequest)
teStorage, okStorage := storage.(TokenExchangeStorage)
if okReq && okStorage {
userInfo := new(oidc.UserInfo)
err := teStorage.SetUserinfoFromTokenExchangeRequest(ctx, userInfo, tokenExchangeRequest)
if err != nil {
return "", err
}
claims.SetUserInfo(userInfo)
} else if len(scopes) > 0 {
userInfo := new(oidc.UserInfo)
err := storage.SetUserinfoFromScopes(ctx, userInfo, request.GetSubject(), request.GetClientID(), scopes)
if err != nil {
return "", err
}
claims.SetUserinfo(userInfo)
claims.SetUserInfo(userInfo)
}
if code != "" {
codeHash, err := oidc.ClaimHash(code, signer.SignatureAlgorithm())
codeHash, err := oidc.ClaimHash(code, signingKey.SignatureAlgorithm())
if err != nil {
return "", err
}
claims.SetCodeHash(codeHash)
claims.CodeHash = codeHash
}
return crypto.Sign(claims, signer.Signer())
signer, err := SignerFromKey(signingKey)
if err != nil {
return "", err
}
return crypto.Sign(claims, signer)
}
func removeUserinfoScopes(scopes []string) []string {

View file

@ -5,8 +5,8 @@ import (
"net/http"
"net/url"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
// ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including
@ -63,15 +63,15 @@ func ParseClientCredentialsRequest(r *http.Request, decoder httphelper.Decoder)
return request, nil
}
// ValidateClientCredentialsRequest validates the refresh_token request parameters including authorization check of the client
// and returns the data representing the original auth request corresponding to the refresh_token
// ValidateClientCredentialsRequest validates the client_credentials request parameters including authorization check of the client
// and returns a TokenRequest and Client implementation to be used in the client_credentials response, resp. creation of the corresponding access_token.
func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (TokenRequest, Client, error) {
storage, ok := exchanger.Storage().(ClientCredentialsStorage)
if !ok {
return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("client_credentials grant not supported")
}
client, err := AuthorizeClientCredentialsClient(ctx, request, exchanger)
client, err := AuthorizeClientCredentialsClient(ctx, request, storage)
if err != nil {
return nil, nil, err
}
@ -84,12 +84,8 @@ func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientC
return tokenRequest, client, nil
}
func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (Client, error) {
if err := AuthorizeClientIDSecret(ctx, request.ClientID, request.ClientSecret, exchanger.Storage()); err != nil {
return nil, err
}
client, err := exchanger.Storage().GetClientByClientID(ctx, request.ClientID)
func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, storage ClientCredentialsStorage) (Client, error) {
client, err := storage.ClientCredentials(ctx, request.ClientID, request.ClientSecret)
if err != nil {
return nil, oidc.ErrInvalidClient().WithParent(err)
}
@ -102,7 +98,7 @@ func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientC
}
func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) {
accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeJWT, creator, client, "")
accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "")
if err != nil {
return nil, err
}

View file

@ -4,8 +4,8 @@ import (
"context"
"net/http"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
// CodeExchange handles the OAuth 2.0 authorization_code grant, including

View file

@ -1,11 +1,399 @@
package op
import (
"errors"
"context"
"net/http"
"net/url"
"strings"
"time"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
// TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange")
func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
RequestError(w, r, errors.New("unimplemented"))
type TokenExchangeRequest interface {
GetAMR() []string
GetAudience() []string
GetResourses() []string
GetAuthTime() time.Time
GetClientID() string
GetScopes() []string
GetSubject() string
GetRequestedTokenType() oidc.TokenType
GetExchangeSubject() string
GetExchangeSubjectTokenType() oidc.TokenType
GetExchangeSubjectTokenIDOrToken() string
GetExchangeSubjectTokenClaims() map[string]interface{}
GetExchangeActor() string
GetExchangeActorTokenType() oidc.TokenType
GetExchangeActorTokenIDOrToken() string
GetExchangeActorTokenClaims() map[string]interface{}
SetCurrentScopes(scopes []string)
SetRequestedTokenType(tt oidc.TokenType)
SetSubject(subject string)
}
type tokenExchangeRequest struct {
exchangeSubjectTokenIDOrToken string
exchangeSubjectTokenType oidc.TokenType
exchangeSubject string
exchangeSubjectTokenClaims map[string]interface{}
exchangeActorTokenIDOrToken string
exchangeActorTokenType oidc.TokenType
exchangeActor string
exchangeActorTokenClaims map[string]interface{}
resource []string
audience oidc.Audience
scopes oidc.SpaceDelimitedArray
requestedTokenType oidc.TokenType
clientID string
authTime time.Time
subject string
}
func (r *tokenExchangeRequest) GetAMR() []string {
return []string{}
}
func (r *tokenExchangeRequest) GetAudience() []string {
return r.audience
}
func (r *tokenExchangeRequest) GetResourses() []string {
return r.resource
}
func (r *tokenExchangeRequest) GetAuthTime() time.Time {
return r.authTime
}
func (r *tokenExchangeRequest) GetClientID() string {
return r.clientID
}
func (r *tokenExchangeRequest) GetScopes() []string {
return r.scopes
}
func (r *tokenExchangeRequest) GetRequestedTokenType() oidc.TokenType {
return r.requestedTokenType
}
func (r *tokenExchangeRequest) GetExchangeSubject() string {
return r.exchangeSubject
}
func (r *tokenExchangeRequest) GetExchangeSubjectTokenType() oidc.TokenType {
return r.exchangeSubjectTokenType
}
func (r *tokenExchangeRequest) GetExchangeSubjectTokenIDOrToken() string {
return r.exchangeSubjectTokenIDOrToken
}
func (r *tokenExchangeRequest) GetExchangeSubjectTokenClaims() map[string]interface{} {
return r.exchangeSubjectTokenClaims
}
func (r *tokenExchangeRequest) GetExchangeActor() string {
return r.exchangeActor
}
func (r *tokenExchangeRequest) GetExchangeActorTokenType() oidc.TokenType {
return r.exchangeActorTokenType
}
func (r *tokenExchangeRequest) GetExchangeActorTokenIDOrToken() string {
return r.exchangeActorTokenIDOrToken
}
func (r *tokenExchangeRequest) GetExchangeActorTokenClaims() map[string]interface{} {
return r.exchangeActorTokenClaims
}
func (r *tokenExchangeRequest) GetSubject() string {
return r.subject
}
func (r *tokenExchangeRequest) SetCurrentScopes(scopes []string) {
r.scopes = scopes
}
func (r *tokenExchangeRequest) SetRequestedTokenType(tt oidc.TokenType) {
r.requestedTokenType = tt
}
func (r *tokenExchangeRequest) SetSubject(subject string) {
r.subject = subject
}
// TokenExchange handles the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange")
func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
tokenExchangeReq, clientID, clientSecret, err := ParseTokenExchangeRequest(r, exchanger.Decoder())
if err != nil {
RequestError(w, r, err)
}
tokenExchangeRequest, client, err := ValidateTokenExchangeRequest(r.Context(), tokenExchangeReq, clientID, clientSecret, exchanger)
if err != nil {
RequestError(w, r, err)
return
}
resp, err := CreateTokenExchangeResponse(r.Context(), tokenExchangeRequest, client, exchanger)
if err != nil {
RequestError(w, r, err)
return
}
httphelper.MarshalJSON(w, resp)
}
// ParseTokenExchangeRequest parses the http request into oidc.TokenExchangeRequest
func ParseTokenExchangeRequest(r *http.Request, decoder httphelper.Decoder) (_ *oidc.TokenExchangeRequest, clientID, clientSecret string, err error) {
err = r.ParseForm()
if err != nil {
return nil, "", "", oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err)
}
request := new(oidc.TokenExchangeRequest)
err = decoder.Decode(request, r.Form)
if err != nil {
return nil, "", "", oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err)
}
var ok bool
if clientID, clientSecret, ok = r.BasicAuth(); ok {
clientID, err = url.QueryUnescape(clientID)
if err != nil {
return nil, "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err)
}
clientSecret, err = url.QueryUnescape(clientSecret)
if err != nil {
return nil, "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err)
}
}
return request, clientID, clientSecret, nil
}
// ValidateTokenExchangeRequest validates the token exchange request parameters including authorization check of the client,
// subject_token and actor_token
func ValidateTokenExchangeRequest(
ctx context.Context,
oidcTokenExchangeRequest *oidc.TokenExchangeRequest,
clientID, clientSecret string,
exchanger Exchanger,
) (TokenExchangeRequest, Client, error) {
if oidcTokenExchangeRequest.SubjectToken == "" {
return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token missing")
}
if oidcTokenExchangeRequest.SubjectTokenType == "" {
return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token_type missing")
}
storage := exchanger.Storage()
teStorage, ok := storage.(TokenExchangeStorage)
if !ok {
return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("token_exchange grant not supported")
}
client, err := AuthorizeTokenExchangeClient(ctx, clientID, clientSecret, exchanger)
if err != nil {
return nil, nil, err
}
if oidcTokenExchangeRequest.RequestedTokenType != "" && !oidcTokenExchangeRequest.RequestedTokenType.IsSupported() {
return nil, nil, oidc.ErrInvalidRequest().WithDescription("requested_token_type is not supported")
}
if !oidcTokenExchangeRequest.SubjectTokenType.IsSupported() {
return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token_type is not supported")
}
if oidcTokenExchangeRequest.ActorTokenType != "" && !oidcTokenExchangeRequest.ActorTokenType.IsSupported() {
return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token_type is not supported")
}
exchangeSubjectTokenIDOrToken, exchangeSubject, exchangeSubjectTokenClaims, ok := GetTokenIDAndSubjectFromToken(ctx, exchanger,
oidcTokenExchangeRequest.SubjectToken, oidcTokenExchangeRequest.SubjectTokenType, false)
if !ok {
return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token is invalid")
}
var (
exchangeActorTokenIDOrToken, exchangeActor string
exchangeActorTokenClaims map[string]interface{}
)
if oidcTokenExchangeRequest.ActorToken != "" {
exchangeActorTokenIDOrToken, exchangeActor, exchangeActorTokenClaims, ok = GetTokenIDAndSubjectFromToken(ctx, exchanger,
oidcTokenExchangeRequest.ActorToken, oidcTokenExchangeRequest.ActorTokenType, true)
if !ok {
return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token is invalid")
}
}
req := &tokenExchangeRequest{
exchangeSubjectTokenIDOrToken: exchangeSubjectTokenIDOrToken,
exchangeSubjectTokenType: oidcTokenExchangeRequest.SubjectTokenType,
exchangeSubject: exchangeSubject,
exchangeSubjectTokenClaims: exchangeSubjectTokenClaims,
exchangeActorTokenIDOrToken: exchangeActorTokenIDOrToken,
exchangeActorTokenType: oidcTokenExchangeRequest.ActorTokenType,
exchangeActor: exchangeActor,
exchangeActorTokenClaims: exchangeActorTokenClaims,
subject: exchangeSubject,
resource: oidcTokenExchangeRequest.Resource,
audience: oidcTokenExchangeRequest.Audience,
scopes: oidcTokenExchangeRequest.Scopes,
requestedTokenType: oidcTokenExchangeRequest.RequestedTokenType,
clientID: client.GetID(),
authTime: time.Now(),
}
err = teStorage.ValidateTokenExchangeRequest(ctx, req)
if err != nil {
return nil, nil, err
}
err = teStorage.CreateTokenExchangeRequest(ctx, req)
if err != nil {
return nil, nil, err
}
return req, client, nil
}
func GetTokenIDAndSubjectFromToken(
ctx context.Context,
exchanger Exchanger,
token string,
tokenType oidc.TokenType,
isActor bool,
) (tokenIDOrToken, subject string, claims map[string]interface{}, ok bool) {
switch tokenType {
case oidc.AccessTokenType:
var accessTokenClaims *oidc.AccessTokenClaims
tokenIDOrToken, subject, accessTokenClaims, ok = getTokenIDAndClaims(ctx, exchanger, token)
claims = accessTokenClaims.Claims
case oidc.RefreshTokenType:
refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token)
if err != nil {
break
}
tokenIDOrToken, subject, ok = token, refreshTokenRequest.GetSubject(), true
case oidc.IDTokenType:
idTokenClaims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, token, exchanger.IDTokenHintVerifier(ctx))
if err != nil {
break
}
tokenIDOrToken, subject, claims, ok = token, idTokenClaims.Subject, idTokenClaims.Claims, true
}
if !ok {
if verifier, ok := exchanger.Storage().(TokenExchangeTokensVerifierStorage); ok {
var err error
if isActor {
tokenIDOrToken, subject, claims, err = verifier.VerifyExchangeActorToken(ctx, token, tokenType)
} else {
tokenIDOrToken, subject, claims, err = verifier.VerifyExchangeSubjectToken(ctx, token, tokenType)
}
if err != nil {
return "", "", nil, false
}
return tokenIDOrToken, subject, claims, true
}
return "", "", nil, false
}
return tokenIDOrToken, subject, claims, true
}
// AuthorizeTokenExchangeClient authorizes a client by validating the client_id and client_secret
func AuthorizeTokenExchangeClient(ctx context.Context, clientID, clientSecret string, exchanger Exchanger) (client Client, err error) {
if err := AuthorizeClientIDSecret(ctx, clientID, clientSecret, exchanger.Storage()); err != nil {
return nil, err
}
client, err = exchanger.Storage().GetClientByClientID(ctx, clientID)
if err != nil {
return nil, oidc.ErrInvalidClient().WithParent(err)
}
return client, nil
}
func CreateTokenExchangeResponse(
ctx context.Context,
tokenExchangeRequest TokenExchangeRequest,
client Client,
creator TokenCreator,
) (_ *oidc.TokenExchangeResponse, err error) {
var (
token, refreshToken, tokenType string
validity time.Duration
)
switch tokenExchangeRequest.GetRequestedTokenType() {
case oidc.AccessTokenType, oidc.RefreshTokenType:
token, refreshToken, validity, err = CreateAccessToken(ctx, tokenExchangeRequest, client.AccessTokenType(), creator, client, "")
if err != nil {
return nil, err
}
tokenType = oidc.BearerToken
case oidc.IDTokenType:
token, err = CreateIDToken(ctx, IssuerFromContext(ctx), tokenExchangeRequest, client.IDTokenLifetime(), "", "", creator.Storage(), client)
if err != nil {
return nil, err
}
// not applicable (see https://datatracker.ietf.org/doc/html/rfc8693#section-2-2-1-2-6)
tokenType = "N_A"
default:
// oidc.JWTTokenType and other custom token types are not supported for issuing.
// In the future it can be considered to have custom tokens generation logic injected via op configuration
// or via expanding Storage interface
oidc.ErrInvalidRequest().WithDescription("requested_token_type is invalid")
}
exp := uint64(validity.Seconds())
return &oidc.TokenExchangeResponse{
AccessToken: token,
IssuedTokenType: tokenExchangeRequest.GetRequestedTokenType(),
TokenType: tokenType,
ExpiresIn: exp,
RefreshToken: refreshToken,
Scopes: tokenExchangeRequest.GetScopes(),
}, nil
}
func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, *oidc.AccessTokenClaims, bool) {
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
if err == nil {
splitToken := strings.Split(tokenIDSubject, ":")
if len(splitToken) != 2 {
return "", "", nil, false
}
return splitToken[0], splitToken[1], nil, true
}
accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
if err != nil {
return "", "", nil, false
}
return accessTokenClaims.JWTID, accessTokenClaims.Subject, accessTokenClaims, true
}

View file

@ -1,24 +1,24 @@
package op
import (
"context"
"errors"
"net/http"
"net/url"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type Introspector interface {
Decoder() httphelper.Decoder
Crypto() Crypto
Storage() Storage
AccessTokenVerifier() AccessTokenVerifier
AccessTokenVerifier(context.Context) AccessTokenVerifier
}
type IntrospectorJWTProfile interface {
Introspector
JWTProfileVerifier() JWTProfileVerifier
JWTProfileVerifier(context.Context) JWTProfileVerifier
}
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
@ -28,7 +28,7 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *
}
func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) {
response := oidc.NewIntrospectionResponse()
response := new(oidc.IntrospectionResponse)
token, clientID, err := ParseTokenIntrospectionRequest(r, introspector)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
@ -44,43 +44,24 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto
httphelper.MarshalJSON(w, response)
return
}
response.SetActive(true)
response.Active = true
httphelper.MarshalJSON(w, response)
}
func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) {
err = r.ParseForm()
clientID, authenticated, err := ClientIDFromRequest(r, introspector)
if err != nil {
return "", "", errors.New("unable to parse request")
return "", "", err
}
req := new(struct {
oidc.IntrospectionRequest
oidc.ClientAssertionParams
})
if !authenticated {
return "", "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
}
req := new(oidc.IntrospectionRequest)
err = introspector.Decoder().Decode(req, r.Form)
if err != nil {
return "", "", errors.New("unable to parse request")
}
if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" {
profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier())
if err == nil {
return req.Token, profile.Issuer, nil
}
}
clientID, clientSecret, ok := r.BasicAuth()
if ok {
clientID, err = url.QueryUnescape(clientID)
if err != nil {
return "", "", errors.New("invalid basic auth header")
}
clientSecret, err = url.QueryUnescape(clientSecret)
if err != nil {
return "", "", errors.New("invalid basic auth header")
}
if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
return "", "", err
}
return req.Token, clientID, nil
}
return "", "", errors.New("invalid authorization")
return req.Token, clientID, nil
}

View file

@ -5,13 +5,13 @@ import (
"net/http"
"time"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type JWTAuthorizationGrantExchanger interface {
Exchanger
JWTProfileVerifier() JWTProfileVerifier
JWTProfileVerifier(context.Context) JWTProfileVerifier
}
// JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1
@ -21,7 +21,7 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati
RequestError(w, r, err)
}
tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier())
tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier(r.Context()))
if err != nil {
RequestError(w, r, err)
return
@ -53,27 +53,65 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (*
return tokenReq, nil
}
// CreateJWTTokenResponse creates
// CreateJWTTokenResponse creates an access_token response for a JWT Profile Grant request
// by default the access_token is an opaque string, but can be specified by implementing the JWTProfileTokenStorage interface
func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) {
id, exp, err := creator.Storage().CreateAccessToken(ctx, tokenRequest)
if err != nil {
return nil, err
}
accessToken, err := CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
if err != nil {
return nil, err
// return an opaque token as default to not break current implementations
tokenType := AccessTokenTypeBearer
// the current CreateAccessToken function, esp. CreateJWT requires an implementation of an AccessTokenClient
client := &jwtProfileClient{
id: tokenRequest.GetSubject(),
}
// by implementing the JWTProfileTokenStorage the storage can specify the AccessTokenType to be returned
tokenStorage, ok := creator.Storage().(JWTProfileTokenStorage)
if ok {
var err error
tokenType, err = tokenStorage.JWTProfileTokenType(ctx, tokenRequest)
if err != nil {
return nil, err
}
}
accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, tokenType, creator, client, "")
if err != nil {
return nil, err
}
return &oidc.AccessTokenResponse{
AccessToken: accessToken,
TokenType: oidc.BearerToken,
ExpiresIn: uint64(exp.Sub(time.Now().UTC()).Seconds()),
ExpiresIn: uint64(validity.Seconds()),
}, nil
}
// ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest
//
//deprecated: use ParseJWTProfileGrantRequest
// deprecated: use ParseJWTProfileGrantRequest
func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) {
return ParseJWTProfileGrantRequest(r, decoder)
}
type jwtProfileClient struct {
id string
}
func (j *jwtProfileClient) GetID() string {
return j.id
}
func (j *jwtProfileClient) ClockSkew() time.Duration {
return 0
}
func (j *jwtProfileClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
func (j *jwtProfileClient) GrantTypes() []oidc.GrantType {
return []oidc.GrantType{
oidc.GrantTypeBearer,
}
}

View file

@ -6,9 +6,9 @@ import (
"net/http"
"time"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/strings"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/strings"
)
type RefreshTokenRequest interface {

View file

@ -5,15 +5,13 @@ import (
"net/http"
"net/url"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type Exchanger interface {
Issuer() string
Storage() Storage
Decoder() httphelper.Decoder
Signer() Signer
Crypto() Crypto
AuthMethodPostSupported() bool
AuthMethodPrivateKeyJWTSupported() bool
@ -21,6 +19,9 @@ type Exchanger interface {
GrantTypeTokenExchangeSupported() bool
GrantTypeJWTAuthorizationSupported() bool
GrantTypeClientCredentialsSupported() bool
GrantTypeDeviceCodeSupported() bool
AccessTokenVerifier(context.Context) AccessTokenVerifier
IDTokenHintVerifier(context.Context) IDTokenHintVerifier
}
func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) {
@ -56,6 +57,11 @@ func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
ClientCredentialsExchange(w, r, exchanger)
return
}
case string(oidc.GrantTypeDeviceCode):
if exchanger.GrantTypeDeviceCodeSupported() {
DeviceAccessToken(w, r, exchanger)
return
}
case "":
RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing"))
return
@ -122,7 +128,7 @@ func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.C
// AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously
// registered public key (JWT Profile)
func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) {
jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier())
jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier(ctx))
if err != nil {
return nil, err
}
@ -136,8 +142,8 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang
return client, nil
}
// ValidateGrantType ensures that the requested grant_type is allowed by the Client
func ValidateGrantType(client Client, grantType oidc.GrantType) bool {
// ValidateGrantType ensures that the requested grant_type is allowed by the client
func ValidateGrantType(client interface{ GrantTypes() []oidc.GrantType }, grantType oidc.GrantType) bool {
if client == nil {
return false
}

View file

@ -7,22 +7,22 @@ import (
"net/url"
"strings"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type Revoker interface {
Decoder() httphelper.Decoder
Crypto() Crypto
Storage() Storage
AccessTokenVerifier() AccessTokenVerifier
AccessTokenVerifier(context.Context) AccessTokenVerifier
AuthMethodPrivateKeyJWTSupported() bool
AuthMethodPostSupported() bool
}
type RevokerJWTProfile interface {
Revoker
JWTProfileVerifier() JWTProfileVerifier
JWTProfileVerifier(context.Context) JWTProfileVerifier
}
func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) {
@ -39,8 +39,8 @@ func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) {
}
var subject string
doDecrypt := true
if canRefreshInfo, ok := revoker.Storage().(CanRefreshTokenInfo); ok && tokenTypeHint != "access_token" {
userID, tokenID, err := canRefreshInfo.GetRefreshTokenInfo(r.Context(), clientID, token)
if tokenTypeHint != "access_token" {
userID, tokenID, err := revoker.Storage().GetRefreshTokenInfo(r.Context(), clientID, token)
if err != nil {
// An invalid refresh token means that we'll try other things (leaving doDecrypt==true)
if !errors.Is(err, ErrInvalidRefreshToken) {
@ -87,7 +87,7 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token
if !ok || !revoker.AuthMethodPrivateKeyJWTSupported() {
return "", "", "", oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported")
}
profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, revokerJWTProfile.JWTProfileVerifier())
profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, revokerJWTProfile.JWTProfileVerifier(r.Context()))
if err == nil {
return req.Token, req.TokenTypeHint, profile.Issuer, nil
}
@ -151,9 +151,9 @@ func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider Use
}
return splitToken[0], splitToken[1], true
}
accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier())
accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
if err != nil {
return "", "", false
}
return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), true
return accessTokenClaims.JWTID, accessTokenClaims.Subject, true
}

View file

@ -6,15 +6,15 @@ import (
"net/http"
"strings"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type UserinfoProvider interface {
Decoder() httphelper.Decoder
Crypto() Crypto
Storage() Storage
AccessTokenVerifier() AccessTokenVerifier
AccessTokenVerifier(context.Context) AccessTokenVerifier
}
func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) {
@ -34,7 +34,7 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
http.Error(w, "access token invalid", http.StatusUnauthorized)
return
}
info := oidc.NewUserInfo()
info := new(oidc.UserInfo)
err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin"))
if err != nil {
httphelper.MarshalJSONWithStatus(w, err, http.StatusForbidden)
@ -81,9 +81,9 @@ func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider
}
return splitToken[0], splitToken[1], true
}
accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier())
accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
if err != nil {
return "", "", false
}
return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), true
return accessTokenClaims.JWTID, accessTokenClaims.Subject, true
}

View file

@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type AccessTokenVerifier interface {
@ -18,8 +18,6 @@ type accessTokenVerifier struct {
maxAgeIAT time.Duration
offset time.Duration
supportedSignAlgs []string
maxAge time.Duration
acr oidc.ACRVerifier
keySet oidc.KeySet
}
@ -67,29 +65,29 @@ func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTok
return verifier
}
// VerifyAccessToken validates the access token (issuer, signature and expiration)
func VerifyAccessToken(ctx context.Context, token string, v AccessTokenVerifier) (oidc.AccessTokenClaims, error) {
claims := oidc.EmptyAccessTokenClaims()
// VerifyAccessToken validates the access token (issuer, signature and expiration).
func VerifyAccessToken[C oidc.Claims](ctx context.Context, token string, v AccessTokenVerifier) (claims C, err error) {
var nilClaims C
decrypted, err := oidc.DecryptToken(token)
if err != nil {
return nil, err
return nilClaims, err
}
payload, err := oidc.ParseToken(decrypted, claims)
payload, err := oidc.ParseToken(decrypted, &claims)
if err != nil {
return nil, err
return nilClaims, err
}
if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil {
return nil, err
return nilClaims, err
}
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
return nil, err
return nilClaims, err
}
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
return nil, err
return nilClaims, err
}
return claims, nil

View file

@ -0,0 +1,70 @@
package op_test
import (
"context"
"fmt"
tu "github.com/zitadel/oidc/v2/internal/testutil"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
)
// MyCustomClaims extends the TokenClaims base,
// so it implements the oidc.Claims interface.
// Instead of carrying a map, we add needed fields// to the struct for type safe access.
type MyCustomClaims struct {
oidc.TokenClaims
NotBefore oidc.Time `json:"nbf,omitempty"`
CodeHash string `json:"c_hash,omitempty"`
SessionID string `json:"sid,omitempty"`
Scopes []string `json:"scope,omitempty"`
AccessTokenUseNumber int `json:"at_use_nbr,omitempty"`
Foo string `json:"foo,omitempty"`
Bar *Nested `json:"bar,omitempty"`
}
// Nested struct types are also possible.
type Nested struct {
Count int `json:"count,omitempty"`
Tags []string `json:"tags,omitempty"`
}
/*
accessToken carries the following claims. foo and bar are custom claims
{
"aud": [
"unit",
"test"
],
"bar": {
"count": 22,
"tags": [
"some",
"tags"
]
},
"exp": 4802234675,
"foo": "Hello, World!",
"iat": 1678097014,
"iss": "local.com",
"jti": "9876",
"nbf": 1678097014,
"sub": "tim@local.com"
}
*/
const accessToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOlsidW5pdCIsInRlc3QiXSwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiZXhwIjo0ODAyMjM0Njc1LCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MDk3MDE0LCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MDk3MDE0LCJzdWIiOiJ0aW1AbG9jYWwuY29tIn0.OUgk-B7OXjYlYFj-nogqSDJiQE19tPrbzqUHEAjcEiJkaWo6-IpGVfDiGKm-TxjXQsNScxpaY0Pg3XIh1xK6TgtfYtoLQm-5RYw_mXgb9xqZB2VgPs6nNEYFUDM513MOU0EBc0QMyqAEGzW-HiSPAb4ugCvkLtM1yo11Xyy6vksAdZNs_mJDT4X3vFXnr0jk0ugnAW6fTN3_voC0F_9HQUAkmd750OIxkAHxAMvEPQcpbLHenVvX_Q0QMrzClVrxehn5TVMfmkYYg7ocr876Bq9xQGPNHAcrwvVIJqdg5uMUA38L3HC2BEueG6furZGvc7-qDWAT1VR9liM5ieKpPg`
func ExampleVerifyAccessToken_customClaims() {
v := op.NewAccessTokenVerifier("local.com", tu.KeySet{})
// VerifyAccessToken can be called with the *MyCustomClaims.
claims, err := op.VerifyAccessToken[*MyCustomClaims](context.TODO(), accessToken, v)
if err != nil {
panic(err)
}
// Here we have typesafe access to the custom claims
fmt.Println(claims.Foo, claims.Bar.Count, claims.Bar.Tags)
// Output: Hello, World! 22 [some tags]
}

View file

@ -0,0 +1,126 @@
package op
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tu "github.com/zitadel/oidc/v2/internal/testutil"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
func TestNewAccessTokenVerifier(t *testing.T) {
type args struct {
issuer string
keySet oidc.KeySet
opts []AccessTokenVerifierOpt
}
tests := []struct {
name string
args args
want AccessTokenVerifier
}{
{
name: "simple",
args: args{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
},
want: &accessTokenVerifier{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
},
},
{
name: "with signature algorithm",
args: args{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
opts: []AccessTokenVerifierOpt{
WithSupportedAccessTokenSigningAlgorithms("ABC", "DEF"),
},
},
want: &accessTokenVerifier{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
supportedSignAlgs: []string{"ABC", "DEF"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewAccessTokenVerifier(tt.args.issuer, tt.args.keySet, tt.args.opts...)
assert.Equal(t, tt.want, got)
})
}
}
func TestVerifyAccessToken(t *testing.T) {
verifier := &accessTokenVerifier{
issuer: tu.ValidIssuer,
maxAgeIAT: 2 * time.Minute,
offset: time.Second,
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
keySet: tu.KeySet{},
}
tests := []struct {
name string
tokenClaims func() (string, *oidc.AccessTokenClaims)
wantErr bool
}{
{
name: "success",
tokenClaims: tu.ValidAccessToken,
},
{
name: "parse err",
tokenClaims: func() (string, *oidc.AccessTokenClaims) { return "~~~~", nil },
wantErr: true,
},
{
name: "invalid signature",
tokenClaims: func() (string, *oidc.AccessTokenClaims) { return tu.InvalidSignatureToken, nil },
wantErr: true,
},
{
name: "wrong issuer",
tokenClaims: func() (string, *oidc.AccessTokenClaims) {
return tu.NewAccessToken(
"foo", tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidJWTID, tu.ValidClientID,
tu.ValidSkew,
)
},
wantErr: true,
},
{
name: "expired",
tokenClaims: func() (string, *oidc.AccessTokenClaims) {
return tu.NewAccessToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.Add(-time.Hour), tu.ValidJWTID, tu.ValidClientID,
tu.ValidSkew,
)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token, want := tt.tokenClaims()
got, err := VerifyAccessToken[*oidc.AccessTokenClaims](context.Background(), token, verifier)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
return
}
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got, want)
})
}
}

View file

@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type IDTokenHintVerifier interface {
@ -73,41 +73,41 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHi
}
// VerifyIDTokenHint validates the id token according to
//https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func VerifyIDTokenHint(ctx context.Context, token string, v IDTokenHintVerifier) (oidc.IDTokenClaims, error) {
claims := oidc.EmptyIDTokenClaims()
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v IDTokenHintVerifier) (claims C, err error) {
var nilClaims C
decrypted, err := oidc.DecryptToken(token)
if err != nil {
return nil, err
return nilClaims, err
}
payload, err := oidc.ParseToken(decrypted, claims)
payload, err := oidc.ParseToken(decrypted, &claims)
if err != nil {
return nil, err
return nilClaims, err
}
if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil {
return nil, err
return nilClaims, err
}
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
return nil, err
return nilClaims, err
}
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
return nil, err
return nilClaims, err
}
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
return nil, err
return nilClaims, err
}
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
return nil, err
return nilClaims, err
}
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
return nil, err
return nilClaims, err
}
return claims, nil
}

View file

@ -0,0 +1,161 @@
package op
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tu "github.com/zitadel/oidc/v2/internal/testutil"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
func TestNewIDTokenHintVerifier(t *testing.T) {
type args struct {
issuer string
keySet oidc.KeySet
opts []IDTokenHintVerifierOpt
}
tests := []struct {
name string
args args
want IDTokenHintVerifier
}{
{
name: "simple",
args: args{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
},
want: &idTokenHintVerifier{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
},
},
{
name: "with signature algorithm",
args: args{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
opts: []IDTokenHintVerifierOpt{
WithSupportedIDTokenHintSigningAlgorithms("ABC", "DEF"),
},
},
want: &idTokenHintVerifier{
issuer: tu.ValidIssuer,
keySet: tu.KeySet{},
supportedSignAlgs: []string{"ABC", "DEF"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewIDTokenHintVerifier(tt.args.issuer, tt.args.keySet, tt.args.opts...)
assert.Equal(t, tt.want, got)
})
}
}
func TestVerifyIDTokenHint(t *testing.T) {
verifier := &idTokenHintVerifier{
issuer: tu.ValidIssuer,
maxAgeIAT: 2 * time.Minute,
offset: time.Second,
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
maxAge: 2 * time.Minute,
acr: tu.ACRVerify,
keySet: tu.KeySet{},
}
tests := []struct {
name string
tokenClaims func() (string, *oidc.IDTokenClaims)
wantErr bool
}{
{
name: "success",
tokenClaims: tu.ValidIDToken,
},
{
name: "parse err",
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
wantErr: true,
},
{
name: "invalid signature",
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
wantErr: true,
},
{
name: "wrong issuer",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
"foo", tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "expired",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "wrong IAT",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "",
)
},
wantErr: true,
},
{
name: "wrong acr",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
"else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "expired auth",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime.Add(-time.Hour), tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token, want := tt.tokenClaims()
got, err := VerifyIDTokenHint[*oidc.IDTokenClaims](context.Background(), token, verifier)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
return
}
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got, want)
})
}
}

View file

@ -8,7 +8,7 @@ import (
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/oidc"
)
type JWTProfileVerifier interface {
@ -104,7 +104,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif
}
type jwtProfileKeyStorage interface {
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
}
func SubjectIsIssuer(request *oidc.JWTTokenRequest) error {
@ -122,7 +122,7 @@ type jwtProfileKeySet struct {
// VerifySignature implements oidc.KeySet by getting the public key from Storage implementation
func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
keyID, _ := oidc.GetKeyIDAndAlg(jws)
key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.clientID)
key, err := k.storage.GetKeyByIDAndClientID(ctx, keyID, k.clientID)
if err != nil {
return nil, fmt.Errorf("error fetching keys: %w", err)
}