Repository navigation
Replies: 2 comments
Revision 1: the open questions answered, a key set the end user plugs in,
|
| Before the reset | With this proposal |
|---|---|
| a JWT verifier written by hand, on golang-jwt and a hand-loaded public key | jwt.NewVerifier[Claims](s, settings, keys), with keys from a jwt.RemoteKeySet or a KeySet of the end user's, held and rotated |
Open questions
All three are answered above.
References
These are added:
- grpc-go's
authzpolicies, whose rules match full method names; go-grpc-middleware'sauthandselectorinterceptors, and its example server; Istio's authorization policypaths - grpc-go's
insecure.NewCredentials - go-oidc's
KeySetandRemoteKeySet
Closing: authentication and authorization have landedThe revision's stories are done:
Revision 1 left no open questions. The rest is out of scope or belongs to other proposals, as "Out of scope" lists:
|
Uh oh!
There was an error while loading. Please reload this page.
Note
Part of the humus re-evaluation; see Proposal: humus on the bedrock process model, wave 5, which revision 7 planned. It replaces REST security, which #485 deleted. It builds on wave 1 (the lifecycle, configuration, OpenTelemetry without globals and humustest), on HTTP server foundation and gRPC services, and on two wave-4 proposals: OpenAPI endpoint DSL, whose
openapi.Securityit adds, and outbound HTTP clients, with whichauth/jwtfetches keys. It depends on no other wave-5 proposal: neither Proposal: failure injection nor Proposal: humus's own metrics.Summary
A new package,
auth, authenticates and authorizes the requests a humus service serves, over HTTP and gRPC alike:auth.Bearer("bearer", verify)declares a way to authenticate a request, andverifyturns the credential into the end user's own principal type, such as theirUser. A handler reads it back withbearer.From(ctx), typed, with no context key of the end user's. There is one constructor for each of OpenAPI's security scheme types: bearer, Basic, an API key in a header, query parameter or cookie, mutual TLS, OAuth2 and OpenID Connect. Every scheme verifies: there is no scheme that only documents.auth.Require(bearer),auth.RequireScopes(bearer, "carts:write")for a principal that has scopes,auth.All(a, b)for both, andauth.Anonymous()for optional authentication. A list of requirements is met by any one of them, as OpenAPI'ssecurityis.openapi.Security(reqs...), anAPIOption, for an OpenAPI API: given toNewAPIit is every operation's, given to an operation it replaces the API's, and the document lists the schemes and requirements;auth.NewHandler(s, h, answers, reqs...)for a plainhttp.Handlerserved byhttpserver;grpcauth.Interceptors(s, rules...)for agrpcserverService, per method.Unauthenticated, with RFC 9110'sWWW-Authenticatechallenges. One authenticated without a scope it needs is a 403, orPermissionDenied. A verifier's error that doesn't wrapauth.ErrUnauthenticated, such as a directory outage, is the server's: never a 401. Every answer's body is the end user's.auth/jwtverifies JWT bearer tokens against an identity provider's JSON Web Key Set, which it fetches with the end user'shttpclient. It is on go-jose v4, which grpc-go already brings into every humus module graph.Background
REST security before the reset was mostly documentation. v0.15.0's
restattached a scheme to a parameter:rest.Header("Authorization", rest.Required(), rest.JWTAuth("jwt", verifier))(rest/parameter.go), and the authentication page described five schemes. OnlyJWTAuthverified anything: it called aJWTVerifier,Verify(ctx, token) (context.Context, error), and answered a missing or malformed header with a 400 and the verifier's error with a 401. The rest added a security scheme to the document and left verification to each handler:APIKeydocumented the key, and the handler looked it up (L423);BasicAuthdocumented Basic as anapiKeyscheme, and the handler decoded the header itself (L443);OAuth2documented no flows (L569);OpenIDConnectignored its name and always wroteopenid-connect(L592);MutualTLSpanicked: "swaggest/openapi3 does not support this" (L555).A verifier put claims in the context with
context.WithValue, under string keys in the docs' examples, and a role check was a hand-written transform.The open issues asked for verification. story(rest): provide jwt validation out of the box #257 asked for JWT validation out of the box, with JWKS fetching behind an interface; story(rest): provide oidc validation out of the box #262 for OIDC validation; story(rest): add argument for api key transformer #277, story(rest): add required argument for basic auth transformer #278, story(rest): allow users to register specific oauth2 flows and transformers #280 and story(rest): implement openid transformer #281 for a verifying "transformer" on the API key, Basic, OAuth2 and OpenID Connect schemes. story(rest): add options for setting additional openapi values #283 asked for OpenAPI's optional fields, which
openapi/v3now has. The reset closed all of them, and the charter made them this proposal's background.gRPC had no authentication.
grpcserver's doc shows the end user's own interceptor,grpc.ChainUnaryInterceptor(authenticate)(grpc/grpcserver/doc.go#L20). Outbound gRPC clients left per-RPC credentials to the end user'sopts, and outbound HTTP clients left credentials to service code.What waves 3 and 4 hand on.
httpserverandgrpcserverserve TLS fromSettings.TLS, so a client certificate is the end user'sClientAuthto ask for, and the verified chains reach every request.grpcserverchains the end user's interceptors inside its own: afterotelgrpchas started the span, and inside panic recovery and the stream cancellation of shutdown.openapi/v3makes every answer the end user's:RequestErrorsfor what it rejects,Errorand a catch-all for the handler's errors. Its revision 1 named this proposal's part: "openapi.Security(…), anAPIOption. Given toNewAPI, it is every operation's requirement. Given to an operation, it replaces the API's."httpclientdials through the Host, with retries and a breaker, and is what fetches an identity provider's keys.go-jose is already in the module graph. grpc-go v1.84.0, which the core module requires for the OTLP exporters, requires
github.com/go-jose/go-jose/v4v4.1.4. go-jose has no dependencies of its own.Proposed API
The package is in the core module, and uses only the standard library and OpenTelemetry's API.
openapi/v3andgrpcauthread aRequirementthroughinternal/authn, the engine the three share.authexports nothing for them, only what the end user writes.openapi/v3grpc/grpcauthauth/jwtauth/jwtis in the core module. It addsgithub.com/go-jose/go-jose/v4to the core module'sgo.modas a direct requirement, at v4.1.6. Itsjwtpackage parses a compact JWS against an algorithm allowlist, and validates the registered claims.How a request is authenticated
The engine is the same for the three. Given a list of requirements:
ErrUnauthenticatedis. Examples:Authorization: Bearerwith no token, twoAuthorizationheaders, Basic credentials that aren't base64 or have no colon, an API key given twice or empty.Anonymousis met when no scheme in the list rejects the request's credentials. To know that, it verifies every credential the request carries.The context of a request that is served carries the principals of the requirement it met, and only those:
Fromof any other scheme returns false.Answers
openapi.Securityauth.NewHandlergrpcauthRequestErrors, status 401,Errauth.ErrUnauthenticatedAnswers.Unauthenticatedcodes.Unauthenticated, "unauthenticated"RequestErrors, status 403,Errauth.ErrForbiddenAnswers.Forbiddencodes.PermissionDenied, "forbidden"Errorthat matches, else the catch-all, which records itAnswers.Failedcodes.Internal, "authentication failed"operation panickedhttpserver, which records it and abortsgrpcserver's recovery:codes.InternalErris the sentinel, soproblem.FromRequestError's detail is "unauthenticated", not "token expired at 10:04" or "no user alice@example.com". gRPC's messages are fixed. A verifier's reason is the end user's to log or put on the span, inverify.openapianswers a verifier's error as the handler's. So an end user whose verifier returns aDirectoryUnavailablegivesopenapi.Error[DirectoryUnavailable](http.StatusServiceUnavailable, enc), and clients retry a 503 rather than drop their token.status.Error(codes.Unavailable, …)for a directory outage getsUnavailable, whichgrpcclient's default classification retries. Its message is the verifier's, which is the end user's to choose.WWW-AuthenticateA 401 sets one challenge for each scheme of the list that HTTP authentication defines, in order, without repeats:
Bearerfor Bearer, OAuth2 and OpenID Connect together, orBearer error="invalid_token"when one of them rejected the request's token, as RFC 6750 §3 has it. There is norealm.Basic realm="<realm>", charset="UTF-8", as RFC 7617 has it.A 403 sets
Bearer error="insufficient_scope", scope="<the scopes>"when a bearer scheme lacked them, and nothing otherwise. An API key and mutual TLS have no HTTP challenge, so a 401 for a list of only those carries none: RFC 9110 asks for at least one, and no standard challenge exists for them.openapisets the header before it callsRequestErrors' encoder, as it setsAllowbeforeMethodNotAllowed.Where each credential is read
Bearer,OAuth2,OpenIDConnectAuthorization: Bearer <token>authorizationmetadata, the same valueBasicAuthorization: Basic <base64>authorizationmetadata, the same valueAPIKeyHeaderAPIKeyQuery,APIKeyCookieInterceptorserrorMutualTLSr.TLS.VerifiedChainscredentials.TLSInfoMutual TLS needs the server's
Settings.TLSto verify client certificates:ClientAuthset toVerifyClientCertIfGiven, so the same server also serves requests that authenticate otherwise, orRequireAndVerifyClientCert. A certificate that is only requested, not verified, is no credential.openapi's requests and documentAuthentication comes first. An operation with requirements authenticates the request before anything else. Only then does it decode its parameters, check the Content-Type and read the body. So an unauthenticated client gets a 401, not a 400 that says which parameter it got wrong, and its body is never read. A request
Unmatchedanswers isn't authenticated: it matched no operation.The document lists the schemes.
NewAPIcollects every scheme of every requirement intocomponents.securitySchemes, keyed by its name, so a requirement can't name a scheme the document lacks:Bearer{"type": "http", "scheme": "bearer", "bearerFormat": <Format>}Basic{"type": "http", "scheme": "basic"}APIKey…{"type": "apiKey", "name": <param>, "in": "header" | "query" | "cookie"}MutualTLS{"type": "mutualTLS"}OAuth2{"type": "oauth2", "flows": {…}}, with each flow's URLs and scopesOpenIDConnect{"type": "openIdConnect", "openIdConnectUrl": <discovery>}Securitygiven toNewAPIis the document's top-levelsecurity.Securitylists it under itssecurity,[]forSecurity().Anonymousis{}.RequestErrors' E at 401, and at 403 when one of its requirements names a scope.The document is secured like the API. It lists every operation, which is what
grpcserverkeeps from anyone who can reach its port by registering no reflection. SoDocumenttakes the API'sSecurity, andDocument(path, openapi.Security())serves it to anyone. Open question 2.NewAPIreports the mistakes, in the one error it returns, never as a panic:SchemeorRequirement, and a nil verify function;.,-and_, which OpenAPI requires of a components key;All;Formaton a scheme that isn'tBearer;Basicrealm that is empty or holds a quote;OAuth2with no flow, a flow without the URLs it needs, and a scope that no flow declares;OpenIDConnectwhose discovery URL isn't absolute;Errorat 401 on an operation that can answer 401, and at 403 on one that can answer 403, as for 400;Documentthat isn'tSecurity.Probes and other open paths
httphealth.NewService,grpchealth.NewService) has no authentication, as exposing health probes decided.openapi.Securitycovers the API's operations, andauth.NewHandlercovers only the handler it is given. The mux next to them, with the probes, is the end user's.grpcauth, like every RPC. The end user names Health'sCheck,WatchandListwithgrpcauth.Methodand no requirement. The docs show it. If they don't, the kubelet's probes getUnauthenticated, and the pod never turns ready. That fails at the first deploy, never silently.Default. So the schema is served only to authenticated clients.Verifying JWTs
(*jwt.Verifier[C]).Verifyis a verify function for any bearer scheme. For each token:Algorithms. Anything else is rejected,noneandHS256among them.kid, and the key set must have that key. A token withoutkidis rejected.C.Cis the end user's, andauth/jwtreads nothing from it.issmust beIssuer,audmust holdAudience,expmust be present and later than now minusLeeway, andnbfandiatmust not be later than now plusLeeway. Now istime.Now(), so a test moves it withsynctest.Every rejection wraps
auth.ErrUnauthenticated.Keys are fetched when they are needed, never at startup. An identity provider that is down when the pod starts doesn't fail
Init. A failedInitwould put the pod in a crash loop, and an API whose tokens can't be verified still serves its public operations. The key set is fetched:kidthe set doesn't have, which is how an identity provider's key rotation reaches the service;MaxKeyAge, so a key the provider withdrew stops verifying.The fetches are at most one per
MinRefreshInterval, whatever the tokens. So a client that sends tokens with made-upkids can't make the service fetch on every request: within the interval, an unknownkidis rejected. Concurrent requests share one fetch. The fetch usescontext.WithoutCancelof the request's context, so it is in the request's trace, and a client that goes away doesn't fail it for the requests waiting on it.httpclient'sTimeoutbounds it.Failures. The body is read up to 1 MiB, as a JSON Web Key Set. A key without
kid, withuseother thansig, with private parts, or of a type go-jose doesn't read is skipped, rather than failing the set.MinRefreshIntervalof the failed fetch, it fails at once, without fetching again.JWKSURLmust behttps. A key set fetched in plain text would let anyone on the path add a key and sign their own tokens. The client must trust the provider through itsTLS.RootCAs:httpclientrefusesInsecureSkipVerify, andhttpclientrefuses a request to another origin, so the client is built forJWKSURL's.JWKSURLhttpsURL with a hostCheckJWKSURLIssuerCheckIssuerAudienceCheckAudienceAlgorithmsRS256,PS256,ES256,EdDSACheckAlgorithmsLeeway1mCheckLeewayMinRefreshInterval1mCheckMinRefreshIntervalMaxKeyAge1hMinRefreshIntervalCheckMaxKeyAge; the order inNewVerifierTelemetry
Nothing reads or sets OpenTelemetry's globals.
A refusal isn't recorded. A 401 or 403,
UnauthenticatedorPermissionDenied, is the client's mistake, as aRequestErroris.otelhttpandotelgrpcalready record its status on the request's span and in the duration metric. So a credential-stuffing attempt shows as a rise in 401s, with no log line per attempt.A verifier's failure is recorded once. In
openapi, it is the catch-all'srequest failed, or anError's answer, which isn't logged, as for any handler's error.auth.NewHandlerandgrpcauthrecord:authentication failed, in the request's context, withauth.scheme,error.typeandexception.message, plushttp.request.methodorrpc.method.When the client has gone away, the error goes on the span alone, as
openapi's records do.auth/jwtlogs one WARN record,keys refresh failed, for a refresh that failed while keys were held, withurl.full,error.typeandexception.message. Its fetches are spans and metrics of the end user'shttpclient.No principal is recorded. humus puts no
user.id, subject or key on a span or a record, since that is personal data in most services. A verify function that wants it on the span adds it, fromtrace.SpanFromContext(ctx).No instruments of its own. The refusals are in
http.server.request.durationandrpc.server.call.duration, by status.Panics
authrecovers nothing itself. A verifier runs on the request's goroutine, before the handler, so a panic in it is recovered where a handler's would be.openapianswers it with the catch-all and recordsoperation panicked, as it does for a decoder's.httpserverrecordshandler panickedand aborts the response forauth.NewHandler.grpcserver's recovery answerscodes.Internal.Design decisions
authdeclares schemes and requirements once. An end user with an OpenAPI API and a gRPC Service on one pod uses onebearervalue for both.openapi/v3holdsSecurity, because its options' interfaces have unexported methods.authholdsNewHandler, becausenet/httpis the standard library andopenapireads credentials the same way.grpcauthis in thegrpcmodule, so a pod without gRPC doesn't link grpc-go's server.MutualTLSworks. Other HTTP schemes, such as Digest, HOBA or HTTP message signatures, are out of scope.Scheme[P]carries the end user's type fromverifytoFrom. There is no claims map, nocontext.WithValuewith a key of the end user's, and no type assertion in a handler.Fromis the scheme's own method, so the same call works in anopenapihandler, anhttp.Handlerand a gRPC method.RequireScopesis constrained to aScopedP. So a scope on a principal that can't have one doesn't compile, andauthnever guesses where scopes live in a token:scope,scp,rolesandgroupsare all common. The end user'sHasScopedecides, including hierarchies.ErrUnauthenticatedmarks a rejection, and any other error is the server's. A directory or identity-provider outage answered with a 401 tells clients to drop a valid token and log the user out, and shows on no 5xx alert. A sloppy verifier that returns a plain error for a bad token gets a 500 in its first test, with the error in the record, rather than shipping. Open question 3.Authorization: Basic …to a bearer API needs the challenge, which only a 401 carries.openapiusesRequestErrors, which already answers every request the API rejects, andNewHandlertakeshttp.Handlers, asopenapi.Unmatcheddoes.Erris a sentinel, so the verifier's reason never reaches the client through a wrap that prints it.openapi. Parameters, Content-Type and body are only looked at for a client that may see their errors.grpcauth.Interceptorsreturns both interceptors, and requiresDefault. gRPC has no way to make oneServerOptionof a unary and a stream interceptor. A function returning one of them invites an end user to install it and leave every streaming method open. Two return values make dropping one a visible_.Defaultis required because there is no document to show which methods are open: a method added later is covered by it, andDefault()makes "public unless listed" an explicit choice.grpcauthdoesn't exemptgrpc.health.v1.Healthon its own. An exemption that the end user can't see is a hole, and the failure of forgetting it is loud.auth/jwtis opt-in, likejsoncodecandproblem.authnames no token format, asopenapinames no body format. JWT bearer tokens from an identity provider are what most APIs verify, and story(rest): provide jwt validation out of the box #257 asked for them out of the box. Open question 1.golang-jwt/jwtreads no JSON Web Keys, so it would need a second library or a hand-written JWK parser. lestrrat-go/jwx brings a dozen modules. Signature verification is not code humus should write itself.jwt.SettingstakesJWKSURLandIssueras they are: two settings an operator reads once from the discovery document.OpenIDConnect's discovery URL is for the document, where clients use it. Fetching discovery would be a second fetch to fail and refresh, for values that never change.MaxKeyAgepasses turns a short outage into logging out every user.x/oauth2'sTokenSourcewith the end user'shttpclientcovers it today.Testing
A prototype of
auth, the shared engine,openapi/v3'sSecurity,grpcauthandauth/jwtpassed the tests below, undergo test -race -count=3, on Go 1.27, with go-jose v4.1.6 and grpc-go v1.84.0. Each test runs the end user's whole App inside atesting/synctestbubble, on a humustestHost, served byhttpserverorgrpcserverover TLS with certificates fromhumustest.NewCA. Theauth/jwttests serve a JSON Web Key Set from the end user's own handler, onhttpserver, atidp:8443on the same Host, and fetch it withhttpclient. That is the end user's test of their own verifier, not a fake identity provider. Keys come fromecdsa.GenerateKeywithcrypto/rand, passwords fromrand.Text, and client certificates from the test's CA, so no key, token or password is fixed. Token expiry,MinRefreshIntervalandMaxKeyAgetake no real time. The prototype was in a scratchpad and is not kept, and the sketches below are trimmed from it.An API whose operations answer by the request's credentials:
The identity provider rotates its keys, and a refresh fails:
The prototype's tests also showed the following:
WWW-Authenticate, and onerequest failedrecord whose message holds the verifier's error. A verifier's error that matches anopenapi.Errorgets that answer, such as a 503. A verifier's panic is the catch-all's 500 andoperation panicked.components.securitySchemesholds each scheme as the table above lists it. The top-levelsecurityis[{"bearer":["carts:read"]},{"apiKey":[]}]. An operation's own is listed,[]forSecurity(),[{"bearer":[]},{}]withAnonymous, and[{"oauth":["carts:admin"],"apiKey":[]}]for anAll. A secured operation's responses gain 401, and 403 when it is scoped. A document with every scheme type validates against OpenAPI 3.2's schema-base: Basic, a cookie API key, mutual TLS, OpenID Connect, and OAuth2 with device authorization, authorization code and client credentials flows among them.Document(path, openapi.Security())is served without credentials.NewAPI's errors. Two schemes namedbearer, an OAuth2 scope no flow declares, a nil verify function, and anErrorat 401 on a secured operation are each reported, in one error.auth.NewHandleron a mux. WithClientAuth: tls.VerifyClientCertIfGiven, a client certificate from the test's CA authenticates byMutualTLS, and its common name reaches the handler. Without one, a request is a 401 withBasic realm="admin", charset="UTF-8", and the right password authenticates./healthz, beside it on the mux, answers without credentials.grpcauth. A unary call without metadata isUnauthenticated, and withauthorization: Bearer aliceis served with alice in the handler's context. A server-streaming call that needs a scope isPermissionDeniedfor alice, and served for a principal with it, the principal in the stream's context. A verifier's plain error isInternal"authentication failed", and itsstatus.Error(codes.Unavailable, …)isUnavailable.grpchealth's Check, on the same Service, answersSERVINGwithout credentials.Interceptorsreports a missingDefault, a method given twice, a malformed method name and a query API key.authentication failedrecord. Within a minute, the next token fails too. Once the provider is back and the minute has passed, it is served, after one more fetch.The stories add the tests the prototype didn't have:
auth/jwt's accepts an in-range value with nil and rejects an out-of-range one without naming the field.NewVerifiernames each bad field, reports two together, and rejects aMaxKeyAgebelowMinRefreshIntervaland a nil client.DefaultSettings()with the three required fields set passes. The package never reads abedrock.Environment.Authorizationheaders,Bearerwith no token, Basic that isn't base64 or has no colon, an empty API key and one given twice, are 401s. The scheme name inAuthorizationmatches whatever its case.NewAPIerrors.authentication failedcarriesauth.scheme,error.type,exception.messageand the method, isn't emitted when the client went away, and carries no principal. Nothing is recorded for a 401 or 403.FromafterAlland afterAnonymous, and a scheme verified once when two requirements name it.exporkid, signed with an algorithm not inAlgorithms, withalg: none, or with a futurenbfis rejected. A key set with an encryption key, a private key or a key type go-jose doesn't read keeps its other keys.Each package's example is an
Examplefunction or apackage mainwith anapp_test.go, as the charter requires, andauth,auth/jwtandgrpcauthare held to the 80% coverage gate.Changes to other files
This proposal's stories make these, as it owns authentication and authorization:
openapi/v3.Security,Document's options,RequestError's 401 and 403, authentication before parameters, the document'scomponents.securitySchemes,securityand 401 and 403 responses, theNewAPIchecks, and a "Security" section in its package doc. Proposal: OpenAPI endpoint DSL #760 foresaw the option in its revision 1.grpcserver's doc. Its example's ownauthenticateinterceptor becomesgrpcauth.Interceptors.grpchealth's doc says to name Health's methods public undergrpcauth.auth/jwt(open question 1). It requires a bearer token on every operation, andcarts:writeto change a cart, and serves its document publicly. Its tests serve a key set from a handler of their own, on the humustest Host in its unit tests and onOSHostbeside its Postgres container in its container tests.docs/content/auth.md. It covers schemes and principals, requirements and scopes, the three places, the answers and challenges, leaving probes open, verifying JWTs withauth/jwt, and testing with a key set served on a humustest Host. The OpenAPI, gRPC and health pages link to it, and the gRPC page shows the health rules.go.modrequiresgithub.com/go-jose/go-jose/v4v4.1.6 directly.httpserver,httphealth,grpcclient,httpclient,humustestandtelemetrydon't change. No lint rule changes.Migration from today
rest.Header("Authorization", rest.Required(), rest.JWTAuth("jwt", verifier))bearer := auth.Bearer("jwt", verify).Format("JWT"), andopenapi.Security(auth.Require(bearer))JWTVerifier.Verify(ctx, token) (context.Context, error), claims put in the contextverify(ctx, token) (P, error), read withbearer.From(ctx)jwt.NewVerifier[Claims](s, settings, idpClient), with keys fetched and rotatedrest.APIKey("api-key")on a header, query parameter or cookie, checked in the handlerauth.APIKeyHeader,APIKeyQueryorAPIKeyCookie, verified before the handlerrest.BasicAuth("basic"), documented as an API key, decoded in the handlerauth.Basic(name, realm, verify), documented ashttp/basicrest.OAuth2("oauth2"), with no flowsauth.OAuth2(name, auth.Flows{…}, verify)rest.OpenIDConnect(name, url), always namedopenid-connectauth.OpenIDConnect(name, url, verify)rest.MutualTLS, which panickedauth.MutualTLS(name, verify)requireRoletransformauth.RequireScopes(bearer, "admin"), with the principal'sHasScopeJWTAuthwithoutRequired(), for optional authenticationopenapi.Security(auth.Require(bearer), auth.Anonymous())auth.ErrUnauthenticated: 401; any other: the server'sgrpcauth.InterceptorsOpen questions
auth/jwt? As proposed, an opt-in package verifies JWT bearer tokens against a JSON Web Key Set fetched with the end user'shttpclient, on go-jose v4. Without it,authstays format-free, and every end user writes the verifier, with key fetching, rotation and the claims checks, which old humus left to them too. It is the one place this proposal adds a dependency, though go-jose is already in the module graph through grpc-go. Recommendation: ship it, opt-in, asjsoncodecandproblemare.Documenttakes the API'sSecurity, andDocument(path, openapi.Security())makes it public. The alternative is a document that is always public, which needs no option onDocument, but serves the schema of an internal API to anyone who can reach it. Recommendation: secured like the API. That is the choicegrpcservermade by registering no reflection.auth.ErrUnauthenticatedis a 401, and any other is the server's:openapi's catch-all or anError,NewHandler'sFailed, gRPC'sInternalor the error's own status. The alternative answers every error with a 401, as old humus did. That is simpler to write a verifier for, but it turns a directory outage into a 401 that tells clients to drop valid tokens, and shows on no 5xx alert. Recommendation: the server's.Out of scope
httpclientor per-RPC credentials forgrpcclient. Outbound HTTP clients and outbound gRPC clients leave them to service code andopts.typ, and sender-constrained tokens: DPoP, and certificate-bound tokens (RFC 8705).From.References
openapi.Security) and revision 5 (RequestErrors,problem)auth/jwtfetches with, and outbound credentials left out of scopeSettings,DefaultSettingsandCheck<Field>; humustest's revision 2: the in-memory network andNewCArest/parameter.goand its authentication page; story(rest): provide jwt validation out of the box #257, story(rest): provide oidc validation out of the box #262, story(rest): add argument for api key transformer #277, story(rest): add required argument for basic auth transformer #278, story(rest): allow users to register specific oauth2 flows and transformers #280, story(rest): implement openid transformer #281 and story(rest): add options for setting additional openapi values #283WWW-Authenticate; RFC 6750: bearer challenges and their errors; RFC 7617: Basic; RFC 9068: JWT access tokens andscope; RFC 7517 and RFC 7519: JWK and JWT; RFC 9700: the OAuth 2.0 security best current practicejwtpackageUnauthenticatedandPermissionDeniedAll reactions