blob: 231f4d5460c3f81a6f9e6c66cd85f0911d48895f [file]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package auth
import (
"crypto/subtle"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/apache/incubator-devlake/core/models/common"
"github.com/apache/incubator-devlake/helpers/oidchelper"
"github.com/apache/incubator-devlake/server/api/shared"
)
// publicPaths is the set of routes reachable without authentication.
// /auth/userinfo and /auth/logout are public so the UI can poll identity
// and clear its session even when the cookie has lapsed; both handlers
// short-circuit gracefully when no user is set.
var publicPaths = map[string]struct{}{
"/ping": {},
"/ready": {},
"/health": {},
"/version": {},
"/proceed-db-migration": {},
PathMethods: {},
PathLogin: {},
PathCallback: {},
PathLogout: {},
PathUserInfo: {},
}
func OIDCAuthentication() gin.HandlerFunc { return defaultService.OIDCAuthentication() }
func RequireAuth() gin.HandlerFunc { return defaultService.RequireAuth() }
func CSRFProtect() gin.HandlerFunc { return defaultService.CSRFProtect() }
// OIDCAuthentication reads the session cookie, verifies the JWT, and sets
// common.USER on the context. Soft authenticator: invalid/missing cookies
// pass through and RequireAuth decides whether to reject.
func (s *Service) OIDCAuthentication() gin.HandlerFunc {
return func(c *gin.Context) {
if s.cfg == nil || !s.cfg.AuthEnabled {
c.Next()
return
}
if _, ok := shared.GetUser(c); ok {
c.Next()
return
}
if len(s.cfg.SessionSecret) == 0 {
c.Next()
return
}
raw, err := c.Cookie(oidchelper.SessionCookieName)
if err != nil || raw == "" {
c.Next()
return
}
claims, err := oidchelper.ParseSession(s.cfg.SessionSecret, raw)
if err != nil {
s.logger.Debug("invalid session cookie: %v", err)
oidchelper.ClearSessionCookie(c, s.cfg)
c.Next()
return
}
if s.revoked != nil && s.revoked.IsRevoked(claims.ID) {
s.logger.Debug("revoked session presented: jti=%s", claims.ID)
oidchelper.ClearSessionCookie(c, s.cfg)
c.Next()
return
}
c.Set(common.USER, &common.User{
Name: claims.Name,
Email: claims.Email,
})
s.bumpLastSeen(claims.ID)
c.Next()
}
}
// RequireAuth is the terminal gate. It only becomes a no-op when
// AUTH_ENABLED=false is set explicitly.
func (s *Service) RequireAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if s.cfg == nil || !s.cfg.AuthEnabled {
c.Next()
return
}
if isPublicPath(c.Request.URL.Path) {
c.Next()
return
}
if _, ok := shared.GetUser(c); ok {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "unauthorized",
})
}
}
func isPublicPath(path string) bool {
if _, ok := publicPaths[path]; ok {
return true
}
return strings.HasPrefix(path, "/swagger/")
}
// CSRFProtect rejects unsafe (POST/PUT/DELETE/PATCH) requests authenticated
// via the session cookie unless they echo the CSRF cookie back as the
// X-CSRF-Token header. Other auth methods (API key Bearer, oauth2-proxy
// header) are not subject to CSRF; they don't ride on ambient cookies.
//
// SameSite=Lax already blocks the textbook cross-origin form-POST attack;
// this is defense-in-depth for shared-parent-domain deployments and any
// future GET endpoint that is upgraded to a mutation.
func (s *Service) CSRFProtect() gin.HandlerFunc {
return func(c *gin.Context) {
if s.cfg == nil || !s.cfg.AuthEnabled {
c.Next()
return
}
switch c.Request.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
c.Next()
return
}
if isPublicPath(c.Request.URL.Path) {
c.Next()
return
}
// CSRF only applies when the caller is authenticating via the session
// cookie. Bearer tokens and proxy headers aren't replayable cross-site.
if _, err := c.Cookie(oidchelper.SessionCookieName); err != nil {
c.Next()
return
}
cookie, err := c.Cookie(oidchelper.CSRFCookieName)
header := c.GetHeader(oidchelper.CSRFHeaderName)
if err != nil || cookie == "" || header == "" || subtle.ConstantTimeCompare([]byte(cookie), []byte(header)) != 1 {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"success": false,
"message": "csrf token missing or invalid",
})
return
}
c.Next()
}
}