File: resource_server.go

package info (click to toggle)
golang-github-zitadel-oidc 3.44.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,520 kB
  • sloc: makefile: 5
file content (145 lines) | stat: -rw-r--r-- 4,195 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package rs

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

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

type ResourceServer interface {
	IntrospectionURL() string
	TokenEndpoint() string
	HttpClient() *http.Client
	AuthFn() (any, error)
}

type resourceServer struct {
	issuer        string
	tokenURL      string
	introspectURL string
	httpClient    *http.Client
	authFn        func() (any, error)
}

func (r *resourceServer) IntrospectionURL() string {
	return r.introspectURL
}

func (r *resourceServer) TokenEndpoint() string {
	return r.tokenURL
}

func (r *resourceServer) HttpClient() *http.Client {
	return r.httpClient
}

func (r *resourceServer) AuthFn() (any, error) {
	return r.authFn()
}

func NewResourceServerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) {
	authorizer := func() (any, error) {
		return httphelper.AuthorizeBasic(clientID, clientSecret), nil
	}
	return newResourceServer(ctx, issuer, authorizer, option...)
}

func NewResourceServerJWTProfile(ctx context.Context, issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) {
	signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
	if err != nil {
		return nil, err
	}
	authorizer := func() (any, error) {
		assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
		if err != nil {
			return nil, err
		}
		return client.ClientAssertionFormAuthorization(assertion), nil
	}
	return newResourceServer(ctx, issuer, authorizer, options...)
}

func newResourceServer(ctx context.Context, issuer string, authorizer func() (any, error), options ...Option) (*resourceServer, error) {
	rs := &resourceServer{
		issuer:     issuer,
		httpClient: httphelper.DefaultHTTPClient,
	}
	for _, optFunc := range options {
		optFunc(rs)
	}
	if rs.introspectURL == "" || rs.tokenURL == "" {
		config, err := client.Discover(ctx, rs.issuer, rs.httpClient)
		if err != nil {
			return nil, err
		}
		if rs.tokenURL == "" {
			rs.tokenURL = config.TokenEndpoint
		}
		if rs.introspectURL == "" {
			rs.introspectURL = config.IntrospectionEndpoint
		}
	}
	if rs.tokenURL == "" {
		return nil, errors.New("tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
	}
	rs.authFn = authorizer
	return rs, nil
}

func NewResourceServerFromKeyFile(ctx context.Context, issuer, path string, options ...Option) (ResourceServer, error) {
	c, err := client.ConfigFromKeyFile(path)
	if err != nil {
		return nil, err
	}
	return NewResourceServerJWTProfile(ctx, issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
}

type Option func(*resourceServer)

// WithClient provides the ability to set an http client to be used for the resource server
func WithClient(client *http.Client) Option {
	return func(server *resourceServer) {
		server.httpClient = client
	}
}

// WithStaticEndpoints provides the ability to set static token and introspect URL
func WithStaticEndpoints(tokenURL, introspectURL string) Option {
	return func(server *resourceServer) {
		server.tokenURL = tokenURL
		server.introspectURL = introspectURL
	}
}

// Introspect calls the [RFC7662] Token Introspection
// endpoint and returns the response in an instance of type R.
// [*oidc.IntrospectionResponse] can be used as a good example, or use a custom type if type-safe
// access to custom claims is needed.
//
// [RFC7662]: https://www.rfc-editor.org/rfc/rfc7662
func Introspect[R any](ctx context.Context, rp ResourceServer, token string) (resp R, err error) {
	ctx, span := client.Tracer.Start(ctx, "Introspect")
	defer span.End()

	if rp.IntrospectionURL() == "" {
		return resp, errors.New("resource server: introspection URL is empty")
	}
	authFn, err := rp.AuthFn()
	if err != nil {
		return resp, err
	}
	req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
	if err != nil {
		return resp, err
	}

	if err := httphelper.HttpRequest(rp.HttpClient(), req, &resp); err != nil {
		return resp, err
	}
	return resp, nil
}