Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
229
webauthn.go10.9 kB
package bindings import ( "bytes" "context" "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/Southclaws/dt" "github.com/Southclaws/fault" "github.com/Southclaws/fault/fctx" "github.com/Southclaws/fault/fmsg" "github.com/Southclaws/fault/ftag" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/labstack/echo/v4" "github.com/rs/xid" "github.com/Southclaws/storyden/app/resources/account/account_querier" waprovider "github.com/Southclaws/storyden/app/services/authentication/provider/webauthn" "github.com/Southclaws/storyden/app/services/authentication/session" "github.com/Southclaws/storyden/app/transports/http/middleware/session_cookie" "github.com/Southclaws/storyden/app/transports/http/openapi" "github.com/Southclaws/storyden/internal/config" ) const cookieName = "storyden-webauthn-session" var errNoCookie = fault.New("no webauthn session cookie") type WebAuthn struct { cj *session_cookie.Jar si *session.Issuer accountQuery *account_querier.Querier wa *waprovider.Provider address url.URL } func NewWebAuthn( cfg config.Config, si *session.Issuer, accountQuery *account_querier.Querier, cj *session_cookie.Jar, wa *waprovider.Provider, router *echo.Echo, ) WebAuthn { // in order to retain context across the credential request and creation, // a session cookie is used which stores the webauthn session information. router.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if s, err := c.Cookie(cookieName); err == nil { r := base64.NewDecoder(base64.URLEncoding, strings.NewReader(s.Value)) session := &webauthn.SessionData{} if err := json.NewDecoder(r).Decode(&session); err == nil { r := c.Request() ctx := r.Context() ctx = context.WithValue(ctx, "webauthn", session) c.SetRequest(r.WithContext(ctx)) } } return next(c) } }) return WebAuthn{cj, si, accountQuery, wa, cfg.PublicAPIAddress} } func (a *WebAuthn) WebAuthnRequestCredential(ctx context.Context, request openapi.WebAuthnRequestCredentialRequestObject) (openapi.WebAuthnRequestCredentialResponseObject, error) { cred, sessionData, err := a.wa.BeginRegistration(ctx, string(request.AccountHandle)) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } // Encode the session data as a base64 JSON string j, err := json.Marshal(sessionData) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } value := base64.URLEncoding.EncodeToString(j) // save the base64 as a cookie for the WebAuthnMakeCredential call cookie := http.Cookie{ Name: cookieName, Value: value, // Expire this exchange after 10 minutes Expires: time.Now().Add(time.Minute * 10), SameSite: http.SameSiteDefaultMode, Path: "/", Domain: a.address.Hostname(), Secure: true, HttpOnly: true, } return openapi.WebAuthnRequestCredential200JSONResponse{ WebAuthnRequestCredentialOKJSONResponse: openapi.WebAuthnRequestCredentialOKJSONResponse{ Headers: openapi.WebAuthnRequestCredentialOKResponseHeaders{ SetCookie: cookie.String(), }, Body: serialiseWebAuthnCredentialCreationOptions(*cred), }, }, nil } func (a *WebAuthn) WebAuthnMakeCredential(ctx context.Context, request openapi.WebAuthnMakeCredentialRequestObject) (openapi.WebAuthnMakeCredentialResponseObject, error) { c := ctx.Value("webauthn") session, ok := c.(*webauthn.SessionData) if !ok { return nil, fault.Wrap(errNoCookie, fctx.With(ctx), ftag.With(ftag.InvalidArgument), ) } // NOTE: This is a hack due to oapi-codegen not giving us raw JSON. b, err := json.Marshal(request.Body) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } reader := bytes.NewReader(b) cr, err := protocol.ParseCredentialCreationResponseBody(reader) if err != nil { pe := err.(*protocol.Error) ctx = fctx.WithMeta(ctx, "type", pe.Type, "details", pe.Details, "info", pe.DevInfo, ) return nil, fault.Wrap(err, fctx.With(ctx), ftag.With(ftag.InvalidArgument), fmsg.With(pe.DevInfo)) } invitedBy, err := deserialiseInvitationID(request.Params.InvitationId) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } _, accountID, err := a.wa.FinishRegistration(ctx, string(session.UserID), *session, cr, invitedBy) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } t, err := a.si.Issue(ctx, accountID) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } return openapi.WebAuthnMakeCredential200JSONResponse{ AuthSuccessOKJSONResponse: openapi.AuthSuccessOKJSONResponse{ Body: openapi.AuthSuccess{ Id: xid.NilID().String(), }, Headers: openapi.AuthSuccessOKResponseHeaders{ SetCookie: a.cj.Create(*t).String(), }, }, }, nil } func (a *WebAuthn) WebAuthnGetAssertion(ctx context.Context, request openapi.WebAuthnGetAssertionRequestObject) (openapi.WebAuthnGetAssertionResponseObject, error) { cred, sessionData, err := a.wa.BeginLogin(ctx, string(request.AccountHandle)) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } j, err := json.Marshal(sessionData) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } value := base64.URLEncoding.EncodeToString(j) // save the base64 as a cookie for the WebAuthnMakeCredential call cookie := http.Cookie{ Name: cookieName, Value: value, // Expire this exchange after 10 minutes Expires: time.Now().Add(time.Minute * 10), SameSite: http.SameSiteDefaultMode, Path: "/", Domain: a.address.Hostname(), Secure: true, HttpOnly: true, } return openapi.WebAuthnGetAssertion200JSONResponse{ WebAuthnGetAssertionOKJSONResponse: openapi.WebAuthnGetAssertionOKJSONResponse{ Body: serialiseWebAuthnCredentialRequestOptions(cred.Response), Headers: openapi.WebAuthnGetAssertionOKResponseHeaders{ SetCookie: cookie.String(), }, }, }, nil } func (a *WebAuthn) WebAuthnMakeAssertion(ctx context.Context, request openapi.WebAuthnMakeAssertionRequestObject) (openapi.WebAuthnMakeAssertionResponseObject, error) { c := ctx.Value("webauthn") session, ok := c.(*webauthn.SessionData) if !ok { return nil, fault.Wrap(errNoCookie, fctx.With(ctx), ftag.With(ftag.InvalidArgument), ) } // something here is messing up userHandle b, err := json.Marshal(request.Body) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } reader := bytes.NewReader(b) cr, err := protocol.ParseCredentialRequestResponseBody(reader) if err != nil { pe := err.(*protocol.Error) ctx = fctx.WithMeta(ctx, "type", pe.Type, "details", pe.Details, "info", pe.DevInfo, ) return nil, fault.Wrap(err, fctx.With(ctx), ftag.With(ftag.InvalidArgument), fmsg.With(pe.DevInfo)) } _, acc, err := a.wa.FinishLogin(ctx, string(session.UserID), *session, cr) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } t, err := a.si.Issue(ctx, acc.ID) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } return openapi.WebAuthnMakeAssertion200JSONResponse{ AuthSuccessOKJSONResponse: openapi.AuthSuccessOKJSONResponse{ Body: openapi.AuthSuccess{ Id: xid.NilID().String(), }, Headers: openapi.AuthSuccessOKResponseHeaders{ SetCookie: a.cj.Create(*t).String(), }, }, }, nil } func serialiseWebAuthnCredentialCreationOptions(cred protocol.CredentialCreation) openapi.WebAuthnPublicKeyCreationOptions { rp := openapi.PublicKeyCredentialRpEntity{ Id: cred.Response.RelyingParty.ID, Name: cred.Response.RelyingParty.Name, } user := openapi.PublicKeyCredentialUserEntity{ DisplayName: cred.Response.User.DisplayName, Id: fmt.Sprint(cred.Response.User.ID), Name: cred.Response.User.Name, } pubKeyCredParams := dt.Map(cred.Response.Parameters, func(p protocol.CredentialParameter) openapi.PublicKeyCredentialParameters { alg := float32(p.Algorithm) return openapi.PublicKeyCredentialParameters{ Type: openapi.PublicKeyCredentialType(p.Type), Alg: alg, } }) excludeCredentials := dt.Map(cred.Response.CredentialExcludeList, func(d protocol.CredentialDescriptor) openapi.PublicKeyCredentialDescriptor { transports := dt.Map(d.Transport, func(t protocol.AuthenticatorTransport) openapi.PublicKeyCredentialDescriptorTransports { return openapi.PublicKeyCredentialDescriptorTransports(t) }) return openapi.PublicKeyCredentialDescriptor{ Type: openapi.PublicKeyCredentialType(d.Type), Id: string(d.CredentialID), Transports: &transports, } }) authenticatorSelection := &openapi.AuthenticatorSelectionCriteria{ AuthenticatorAttachment: openapi.AuthenticatorAttachment(cred.Response.AuthenticatorSelection.AuthenticatorAttachment), RequireResidentKey: cred.Response.AuthenticatorSelection.RequireResidentKey, ResidentKey: openapi.ResidentKeyRequirement(cred.Response.AuthenticatorSelection.ResidentKey), UserVerification: (*openapi.UserVerificationRequirement)(&cred.Response.AuthenticatorSelection.UserVerification), } return openapi.WebAuthnPublicKeyCreationOptions{ PublicKey: openapi.PublicKeyCredentialCreationOptions{ Rp: rp, User: user, Challenge: cred.Response.Challenge.String(), PubKeyCredParams: pubKeyCredParams, Timeout: &cred.Response.Timeout, ExcludeCredentials: excludeCredentials, AuthenticatorSelection: authenticatorSelection, Attestation: (*openapi.AttestationConveyancePreference)(&cred.Response.Attestation), Extensions: (*openapi.AuthenticationExtensionsClientInputs)(&cred.Response.Extensions), }, } } func serialiseWebAuthnCredentialRequestOptions(cred protocol.PublicKeyCredentialRequestOptions) openapi.CredentialRequestOptions { allowedCredentials := dt.Map(cred.AllowedCredentials, func(cd protocol.CredentialDescriptor) openapi.PublicKeyCredentialDescriptor { transports := dt.Map(cd.Transport, func(t protocol.AuthenticatorTransport) openapi.PublicKeyCredentialDescriptorTransports { return openapi.PublicKeyCredentialDescriptorTransports(t) }) id := make([]byte, base64.RawStdEncoding.EncodedLen(len(cd.CredentialID))) base64.RawURLEncoding.Encode(id, cd.CredentialID) return openapi.PublicKeyCredentialDescriptor{ Id: string(id), Transports: &transports, Type: openapi.PublicKeyCredentialType(cd.Type), } }) return openapi.CredentialRequestOptions{ PublicKey: openapi.PublicKeyCredentialRequestOptions{ AllowCredentials: &allowedCredentials, Challenge: cred.Challenge.String(), RpId: &cred.RelyingPartyID, Timeout: &cred.Timeout, UserVerification: (*openapi.PublicKeyCredentialRequestOptionsUserVerification)(&cred.UserVerification), }, } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server