Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(op): Server interface #447

Merged
merged 30 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d6a9c0b
first draft of a new server interface
muhlemmer Sep 4, 2023
cf3a87c
allow any response type
muhlemmer Sep 4, 2023
c340ed9
complete interface docs
muhlemmer Sep 4, 2023
4fcda01
refelct the format from the proposal
muhlemmer Sep 6, 2023
2902a81
intermediate commit with some methods implemented
muhlemmer Sep 8, 2023
2b08c53
implement remaining token grant type methods
muhlemmer Sep 11, 2023
6993769
implement remaining server methods
muhlemmer Sep 11, 2023
f4dac05
error handling
muhlemmer Sep 12, 2023
fe3f98a
rewrite auth request validation
muhlemmer Sep 13, 2023
81d42b0
define handlers, routes
muhlemmer Sep 13, 2023
aae3492
input validation and concrete handlers
muhlemmer Sep 20, 2023
c98291a
check if client credential client is authenticated
muhlemmer Sep 21, 2023
af2d294
copy and modify the routes test for the legacy server
muhlemmer Sep 21, 2023
46839e0
run integration tests against both Server and Provider
muhlemmer Sep 21, 2023
6f45991
remove unuse ValidateAuthRequestV2 function
muhlemmer Sep 22, 2023
57e8b19
unit tests for error handling
muhlemmer Sep 22, 2023
b12bb7a
cleanup tokenHandler
muhlemmer Sep 22, 2023
a88181b
move server routest test
muhlemmer Sep 22, 2023
d27be59
unit test authorize
muhlemmer Sep 22, 2023
b7cbe15
handle client credentials in VerifyClient
muhlemmer Sep 25, 2023
f9a4b82
change code exchange route test
muhlemmer Sep 25, 2023
d17e452
finish http unit tests
muhlemmer Sep 25, 2023
abb0bb0
review server interface docs and spelling
muhlemmer Sep 25, 2023
e9c4940
add withClient unit test
muhlemmer Sep 25, 2023
a49ad31
server options
muhlemmer Sep 25, 2023
c6f6a88
cleanup unused GrantType method
muhlemmer Sep 25, 2023
f6cb47f
resolve typo comments
muhlemmer Sep 27, 2023
af22c1a
make endpoints pointers to enable/disable them
muhlemmer Sep 27, 2023
0200c23
jwt profile base work
livio-a Sep 28, 2023
a1a6c19
jwt: correct the test expect
muhlemmer Sep 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions example/server/exampleop/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ var counter atomic.Int64
// SetupServer creates an OIDC server with Issuer=http://localhost:<port>
//
// Use one of the pre-made clients in storage/clients.go or register a new one.
func SetupServer(issuer string, storage Storage, logger *slog.Logger) chi.Router {
func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer bool) chi.Router {
// the OpenID Provider requires a 32-byte key for (token) encryption
// be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test"))
Expand Down Expand Up @@ -77,12 +77,17 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger) chi.Router
registerDeviceAuth(storage, r)
})

handler := http.Handler(provider)
if wrapServer {
handler = op.NewLegacyServer(provider)
}

// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
// is served on the correct path
//
// if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
// then you would have to set the path prefix (/custom/path/)
router.Mount("/", provider)
router.Mount("/", handler)

return router
}
Expand Down
2 changes: 1 addition & 1 deletion example/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {
Level: slog.LevelDebug,
}),
)
router := exampleop.SetupServer(issuer, storage, logger)
router := exampleop.SetupServer(issuer, storage, logger, false)

server := &http.Server{
Addr: ":" + port,
Expand Down
2 changes: 1 addition & 1 deletion example/server/storage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
authMethod: oidc.AuthMethodBasic,
loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
grantTypes: oidc.AllGrantTypes,
livio-a marked this conversation as resolved.
Show resolved Hide resolved
accessTokenType: op.AccessTokenTypeBearer,
devMode: false,
idTokenUserinfoClaimsAssertion: false,
Expand Down
21 changes: 19 additions & 2 deletions pkg/client/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client_test
import (
"bytes"
"context"
"fmt"
"io"
"math/rand"
"net/http"
Expand Down Expand Up @@ -50,14 +51,22 @@ func TestMain(m *testing.M) {
}

func TestRelyingPartySession(t *testing.T) {
for _, wrapServer := range []bool{false, true} {
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
testRelyingPartySession(t, wrapServer)
})
}
}

func testRelyingPartySession(t *testing.T, wrapServer bool) {
t.Log("------- start example OP ------")
targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler
opServer := httptest.NewServer(&dh)
defer opServer.Close()
t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer)

seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
Expand Down Expand Up @@ -101,14 +110,22 @@ func TestRelyingPartySession(t *testing.T) {
}

func TestResourceServerTokenExchange(t *testing.T) {
for _, wrapServer := range []bool{false, true} {
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
testResourceServerTokenExchange(t, wrapServer)
})
}
}

func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
t.Log("------- start example OP ------")
targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler
opServer := httptest.NewServer(&dh)
defer opServer.Close()
t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer)

seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
Expand Down
10 changes: 10 additions & 0 deletions pkg/oidc/token_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ const (
JWTTokenType TokenType = "urn:ietf:params:oauth:token-type:jwt"
)

func (t GrantType) IsSupported() bool {
for _, gt := range AllGrantTypes {
if t == gt {
return true
}
}

return false
}

var AllTokenTypes = []TokenType{
AccessTokenType, RefreshTokenType, IDTokenType, JWTTokenType,
}
Expand Down
18 changes: 9 additions & 9 deletions pkg/op/auth_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
}
ctx := r.Context()
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
Expand Down Expand Up @@ -130,31 +130,31 @@ func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.A

// ParseRequestObject parse the `request` parameter, validates the token including the signature
// and copies the token claims into the auth request
func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) (*oidc.AuthRequest, error) {
func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) error {
requestObject := new(oidc.RequestObject)
payload, err := oidc.ParseToken(authReq.RequestParam, requestObject)
if err != nil {
return nil, err
return err
}

if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID {
return authReq, oidc.ErrInvalidRequest()
return oidc.ErrInvalidRequest()
}
if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType {
return authReq, oidc.ErrInvalidRequest()
return oidc.ErrInvalidRequest()
}
if requestObject.Issuer != requestObject.ClientID {
return authReq, oidc.ErrInvalidRequest()
return oidc.ErrInvalidRequest()
}
if !str.Contains(requestObject.Audience, issuer) {
return authReq, oidc.ErrInvalidRequest()
return oidc.ErrInvalidRequest()
}
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil {
return authReq, err
return err
}
CopyRequestObjectToAuthRequest(authReq, requestObject)
return authReq, nil
return nil
}

// CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request
Expand Down
7 changes: 7 additions & 0 deletions pkg/op/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,10 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au
}
return data.ClientID, false, nil
}

type ClientCredentials struct {
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"` // Client secret from Basic auth or request body
ClientAssertion string `schema:"client_assertion"` // JWT
ClientAssertionType string `schema:"client_assertion_type"`
}
34 changes: 21 additions & 13 deletions pkg/op/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,41 +63,51 @@ func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *htt
}

func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error {
storage, err := assertDeviceStorage(o.Storage())
req, err := ParseDeviceCodeRequest(r, o)
if err != nil {
return err
}

req, err := ParseDeviceCodeRequest(r, o)
response, err := createDeviceAuthorization(r.Context(), req, req.ClientID, o)
if err != nil {
return err
}

httphelper.MarshalJSON(w, response)
return nil
}

func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizationRequest, clientID string, o OpenIDProvider) (*oidc.DeviceAuthorizationResponse, error) {
storage, err := assertDeviceStorage(o.Storage())
if err != nil {
return nil, err
}
config := o.DeviceAuthorization()

deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes)
if err != nil {
return err
return nil, NewStatusError(err, http.StatusInternalServerError)
}
userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
if err != nil {
return err
return nil, NewStatusError(err, http.StatusInternalServerError)
}

expires := time.Now().Add(config.Lifetime)
err = storage.StoreDeviceAuthorization(r.Context(), req.ClientID, deviceCode, userCode, expires, req.Scopes)
err = storage.StoreDeviceAuthorization(ctx, clientID, deviceCode, userCode, expires, req.Scopes)
if err != nil {
return err
return nil, NewStatusError(err, http.StatusInternalServerError)
}

var verification *url.URL
if config.UserFormURL != "" {
if verification, err = url.Parse(config.UserFormURL); err != nil {
return oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for device user form")
err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for device user form")
return nil, NewStatusError(err, http.StatusInternalServerError)
}
} else {
if verification, err = url.Parse(IssuerFromContext(r.Context())); err != nil {
return oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for issuer")
if verification, err = url.Parse(IssuerFromContext(ctx)); err != nil {
err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for issuer")
return nil, NewStatusError(err, http.StatusInternalServerError)
}
verification.Path = config.UserFormPath
}
Expand All @@ -112,9 +122,7 @@ func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvide

verification.RawQuery = "user_code=" + userCode
response.VerificationURIComplete = verification.String()

httphelper.MarshalJSON(w, response)
return nil
return response, nil
}

func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) {
Expand Down
8 changes: 4 additions & 4 deletions pkg/op/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ var DefaultSupportedScopes = []string{

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))
Discover(w, CreateDiscoveryConfig(r.Context(), 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)
func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration {
issuer := IssuerFromContext(ctx)
return &oidc.DiscoveryConfiguration{
Issuer: issuer,
AuthorizationEndpoint: config.AuthorizationEndpoint().Absolute(issuer),
Expand All @@ -49,7 +49,7 @@ func CreateDiscoveryConfig(r *http.Request, config Configuration, storage Discov
ResponseTypesSupported: ResponseTypes(config),
GrantTypesSupported: GrantTypes(config),
SubjectTypesSupported: SubjectTypes(config),
IDTokenSigningAlgValuesSupported: SigAlgorithms(r.Context(), storage),
IDTokenSigningAlgValuesSupported: SigAlgorithms(ctx, storage),
RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config),
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config),
TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config),
Expand Down
8 changes: 4 additions & 4 deletions pkg/op/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ func TestDiscover(t *testing.T) {

func TestCreateDiscoveryConfig(t *testing.T) {
type args struct {
request *http.Request
c op.Configuration
s op.DiscoverStorage
ctx context.Context
c op.Configuration
s op.DiscoverStorage
}
tests := []struct {
name string
Expand All @@ -61,7 +61,7 @@ func TestCreateDiscoveryConfig(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.CreateDiscoveryConfig(tt.args.request, tt.args.c, tt.args.s)
got := op.CreateDiscoveryConfig(tt.args.ctx, tt.args.c, tt.args.s)
assert.Equal(t, tt.want, got)
})
}
Expand Down
Loading