package handler import ( "bufio" "context" "errors" "net" "net/http" "net/http/httputil" "github.com/golang-jwt/jwt/v4" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/rest/token" ) const ( jwtAudience = "aud" jwtExpire = "exp" jwtId = "jti" jwtIssueAt = "iat" jwtIssuer = "iss" jwtNotBefore = "nbf" jwtSubject = "sub" noDetailReason = "no detail reason" ) var ( errInvalidToken = errors.New("invalid auth token") errNoClaims = errors.New("no auth params") ) type ( // A AuthorizeOptions is authorize options. AuthorizeOptions struct { PrevSecret string Callback UnauthorizedCallback } // UnauthorizedCallback defines the method of unauthorized callback. UnauthorizedCallback func(w http.ResponseWriter, r *http.Request, err error) // AuthorizeOption defines the method to customize an AuthorizeOptions. AuthorizeOption func(opts *AuthorizeOptions) ) // Authorize returns an authorize middleware. func Authorize(secret string, opts ...AuthorizeOption) func(http.Handler) http.Handler { var authOpts AuthorizeOptions for _, opt := range opts { opt(&authOpts) } parser := token.NewTokenParser() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tok, err := parser.ParseToken(r, secret, authOpts.PrevSecret) if err != nil { unauthorized(w, r, err, authOpts.Callback) return } if !tok.Valid { unauthorized(w, r, errInvalidToken, authOpts.Callback) return } claims, ok := tok.Claims.(jwt.MapClaims) if !ok { unauthorized(w, r, errNoClaims, authOpts.Callback) return } ctx := r.Context() for k, v := range claims { switch k { case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject: // ignore the standard claims default: ctx = context.WithValue(ctx, k, v) } } next.ServeHTTP(w, r.WithContext(ctx)) }) } } // WithPrevSecret returns an AuthorizeOption with setting previous secret. func WithPrevSecret(secret string) AuthorizeOption { return func(opts *AuthorizeOptions) { opts.PrevSecret = secret } } // WithUnauthorizedCallback returns an AuthorizeOption with setting unauthorized callback. func WithUnauthorizedCallback(callback UnauthorizedCallback) AuthorizeOption { return func(opts *AuthorizeOptions) { opts.Callback = callback } } func detailAuthLog(r *http.Request, reason string) { // discard dump error, only for debug purpose details, _ := httputil.DumpRequest(r, true) logx.Errorf("authorize failed: %s\n=> %+v", reason, string(details)) } func unauthorized(w http.ResponseWriter, r *http.Request, err error, callback UnauthorizedCallback) { writer := newGuardedResponseWriter(w) if err != nil { detailAuthLog(r, err.Error()) } else { detailAuthLog(r, noDetailReason) } // let callback go first, to make sure we respond with user-defined HTTP header if callback != nil { callback(writer, r, err) } // if user not setting HTTP header, we set header with 401 writer.WriteHeader(http.StatusUnauthorized) } type guardedResponseWriter struct { writer http.ResponseWriter wroteHeader bool } func newGuardedResponseWriter(w http.ResponseWriter) *guardedResponseWriter { return &guardedResponseWriter{ writer: w, } } func (grw *guardedResponseWriter) Flush() { if flusher, ok := grw.writer.(http.Flusher); ok { flusher.Flush() } } func (grw *guardedResponseWriter) Header() http.Header { return grw.writer.Header() } // Hijack implements the http.Hijacker interface. // This expands the Response to fulfill http.Hijacker if the underlying http.ResponseWriter supports it. func (grw *guardedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hijacked, ok := grw.writer.(http.Hijacker); ok { return hijacked.Hijack() } return nil, nil, errors.New("server doesn't support hijacking") } func (grw *guardedResponseWriter) Write(body []byte) (int, error) { return grw.writer.Write(body) } func (grw *guardedResponseWriter) WriteHeader(statusCode int) { if grw.wroteHeader { return } grw.wroteHeader = true grw.writer.WriteHeader(statusCode) }