feat: generic Userinfo and Introspect functions (#389)
BREAKING CHANGE: rp.Userinfo and rs.Introspect now require a type parameter.
This commit is contained in:
parent
e43ac6dfdf
commit
d5a9bd6d0e
6 changed files with 136 additions and 18 deletions
|
@ -48,7 +48,7 @@ func main() {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := rs.Introspect(r.Context(), provider, token)
|
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
@ -69,7 +69,7 @@ func main() {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := rs.Introspect(r.Context(), provider, token)
|
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
|
|
@ -435,14 +435,18 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info *oidc.UserInfo)
|
type SubjectGetter interface {
|
||||||
|
GetSubject() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info U)
|
||||||
|
|
||||||
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
||||||
// and calls the userinfo endpoint with the access token
|
// and calls the userinfo endpoint with the access token
|
||||||
// on success it will pass the userinfo into its callback function as well
|
// on success it will pass the userinfo into its callback function as well
|
||||||
func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] {
|
func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] {
|
||||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
||||||
info, err := Userinfo(r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -451,19 +455,25 @@ func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeEx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
// Userinfo will call the OIDC [UserInfo] Endpoint with the provided token and returns
|
||||||
func Userinfo(ctx context.Context, token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) {
|
// the response in an instance of type U.
|
||||||
|
// [*oidc.UserInfo] can be used as a good example, or use a custom type if type-safe
|
||||||
|
// access to custom claims is needed.
|
||||||
|
//
|
||||||
|
// [UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||||
|
func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) {
|
||||||
|
var nilU U
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilU, err
|
||||||
}
|
}
|
||||||
req.Header.Set("authorization", tokenType+" "+token)
|
req.Header.Set("authorization", tokenType+" "+token)
|
||||||
userinfo := new(oidc.UserInfo)
|
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
||||||
return nil, err
|
return nilU, err
|
||||||
}
|
}
|
||||||
if userinfo.Subject != subject {
|
if userinfo.GetSubject() != subject {
|
||||||
return nil, ErrUserInfoSubNotMatching
|
return nilU, ErrUserInfoSubNotMatching
|
||||||
}
|
}
|
||||||
return userinfo, nil
|
return userinfo, nil
|
||||||
}
|
}
|
||||||
|
|
45
pkg/client/rp/userinfo_example_test.go
Normal file
45
pkg/client/rp/userinfo_example_test.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package rp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserInfo struct {
|
||||||
|
Subject string `json:"sub,omitempty"`
|
||||||
|
oidc.UserInfoProfile
|
||||||
|
oidc.UserInfoEmail
|
||||||
|
oidc.UserInfoPhone
|
||||||
|
Address *oidc.UserInfoAddress `json:"address,omitempty"`
|
||||||
|
|
||||||
|
// Foo and Bar are custom claims
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
Bar struct {
|
||||||
|
Val1 string `json:"val_1,omitempty"`
|
||||||
|
Val2 string `json:"val_2,omitempty"`
|
||||||
|
} `json:"bar,omitempty"`
|
||||||
|
|
||||||
|
// Claims are all the combined claims, including custom.
|
||||||
|
Claims map[string]any `json:"-,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserInfo) GetSubject() string {
|
||||||
|
return u.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleUserinfo_custom() {
|
||||||
|
rpo, err := rp.NewRelyingPartyOIDC(context.TODO(), "http://localhost:8080", "clientid", "clientsecret", "http://example.com/redirect", []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := rp.Userinfo[*UserInfo](context.TODO(), "accesstokenstring", "Bearer", "userid", rpo)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(info)
|
||||||
|
}
|
52
pkg/client/rs/introspect_example_test.go
Normal file
52
pkg/client/rs/introspect_example_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package rs_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/client/rs"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntrospectionResponse struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Scope oidc.SpaceDelimitedArray `json:"scope,omitempty"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
TokenType string `json:"token_type,omitempty"`
|
||||||
|
Expiration oidc.Time `json:"exp,omitempty"`
|
||||||
|
IssuedAt oidc.Time `json:"iat,omitempty"`
|
||||||
|
NotBefore oidc.Time `json:"nbf,omitempty"`
|
||||||
|
Subject string `json:"sub,omitempty"`
|
||||||
|
Audience oidc.Audience `json:"aud,omitempty"`
|
||||||
|
Issuer string `json:"iss,omitempty"`
|
||||||
|
JWTID string `json:"jti,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
oidc.UserInfoProfile
|
||||||
|
oidc.UserInfoEmail
|
||||||
|
oidc.UserInfoPhone
|
||||||
|
Address *oidc.UserInfoAddress `json:"address,omitempty"`
|
||||||
|
|
||||||
|
// Foo and Bar are custom claims
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
Bar struct {
|
||||||
|
Val1 string `json:"val_1,omitempty"`
|
||||||
|
Val2 string `json:"val_2,omitempty"`
|
||||||
|
} `json:"bar,omitempty"`
|
||||||
|
|
||||||
|
// Claims are all the combined claims, including custom.
|
||||||
|
Claims map[string]any `json:"-,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleIntrospect_custom() {
|
||||||
|
rss, err := rs.NewResourceServerClientCredentials(context.TODO(), "http://localhost:8080", "clientid", "clientsecret")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := rs.Introspect[*IntrospectionResponse](context.TODO(), rss, "accesstokenstring")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(resp)
|
||||||
|
}
|
|
@ -112,18 +112,24 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.IntrospectionResponse, error) {
|
// 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) {
|
||||||
authFn, err := rp.AuthFn()
|
authFn, err := rp.AuthFn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return resp, err
|
||||||
}
|
}
|
||||||
req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
|
req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return resp, err
|
||||||
}
|
}
|
||||||
resp := new(oidc.IntrospectionResponse)
|
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, &resp); err != nil {
|
||||||
return nil, err
|
return resp, err
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,11 @@ func (u *UserInfo) GetAddress() *UserInfoAddress {
|
||||||
return u.Address
|
return u.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubject implements [rp.SubjectGetter]
|
||||||
|
func (u *UserInfo) GetSubject() string {
|
||||||
|
return u.Subject
|
||||||
|
}
|
||||||
|
|
||||||
type uiAlias UserInfo
|
type uiAlias UserInfo
|
||||||
|
|
||||||
func (u *UserInfo) MarshalJSON() ([]byte, error) {
|
func (u *UserInfo) MarshalJSON() ([]byte, error) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue