feat: add initial oauth2 support

This commit is contained in:
Paul Pan 2024-01-03 00:55:41 +08:00
parent 09611857c9
commit 7faeafb367
Signed by: Paul
GPG Key ID: D639BDF5BA578AF4
16 changed files with 324 additions and 18 deletions

View File

@ -1 +1 @@
1.2.1
1.2.2-dev

View File

@ -17,6 +17,7 @@ import (
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"git.0x7f.app/WOJ/woj-server/internal/web/metrics"
"git.0x7f.app/WOJ/woj-server/internal/web/oauth"
"git.0x7f.app/WOJ/woj-server/internal/web/router"
"github.com/getsentry/sentry-go"
"github.com/samber/do"
@ -75,6 +76,7 @@ func prepareServices(c *cli.Context) *do.Injector {
{ // web helper services
do.Provide(injector, metrics.NewService)
do.Provide(injector, jwt.NewService)
do.Provide(injector, oauth.NewService)
do.Provide(injector, router.NewService)
}

View File

@ -1,8 +1,12 @@
WebServer:
Address: ${WEB_SERVER_ADDRESS}
Port: ${WEB_SERVER_PORT}
PublicBase: ${WEB_SERVER_PUBLIC_BASE}
JwtSigningKey: ${WEB_SERVER_JWT_SIGNING_KEY}
JwtExpireHour: ${WEB_SERVER_JWT_EXPIRE_HOUR}
OAuthDomain: ${WEB_SERVER_OAUTH_DOMAIN}
OAuthClientID: ${WEB_SERVER_OAUTH_CLIENT_ID}
OAuthClientSecret: ${WEB_SERVER_OAUTH_CLIENT_SECRET}
Redis:
Db: ${REDIS_DB}

View File

@ -36,8 +36,12 @@ function check_env() {
check_env "WEB_SERVER_ADDRESS" "0.0.0.0" true
check_env "WEB_SERVER_PORT" 8000 false
check_env "WEB_SERVER_PUBLIC_BASE" "http://127.0.0.1:8000" true
check_env "WEB_SERVER_JWT_SIGNING_KEY" "$(head -n 10 /dev/urandom | md5sum | cut -c 1-32)" true
check_env "WEB_SERVER_JWT_EXPIRE_HOUR" 12 false
check_env "WEB_SERVER_OAUTH_DOMAIN" "" true
check_env "WEB_SERVER_OAUTH_CLIENT_ID" "" true
check_env "WEB_SERVER_OAUTH_CLIENT_SECRET" "" true
check_env "REDIS_DB" 0 false
check_env "REDIS_QUEUE_DB" 1 false

4
go.mod
View File

@ -4,6 +4,7 @@ go 1.20
require (
github.com/TheZeroSlave/zapsentry v1.20.0
github.com/coreos/go-oidc/v3 v3.9.0
github.com/getsentry/sentry-go v0.25.0
github.com/gin-contrib/cors v1.5.0
github.com/gin-contrib/pprof v1.4.0
@ -23,6 +24,7 @@ require (
github.com/urfave/cli/v2 v2.26.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0
golang.org/x/oauth2 v0.13.0
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.4
@ -42,6 +44,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.3 // indirect
github.com/go-openapi/spec v0.20.12 // indirect
@ -92,6 +95,7 @@ require (
golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

11
go.sum
View File

@ -25,6 +25,8 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
@ -58,6 +60,8 @@ github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
@ -90,6 +94,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -324,6 +329,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
@ -350,6 +356,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -389,6 +397,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@ -414,6 +423,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

View File

@ -12,7 +12,7 @@ type loginRequest struct {
Password string `form:"password" json:"password" binding:"required"`
}
type loginResponse struct {
type LoginResponse struct {
Token string `json:"token"`
NickName string `json:"nickname"`
}
@ -25,7 +25,7 @@ type loginResponse struct {
// @Produce json
// @Param username formData string true "username"
// @Param password formData string true "password"
// @Response 200 {object} e.Response[loginResponse] "jwt token and user's nickname"
// @Response 200 {object} e.Response[LoginResponse] "jwt token and user's nickname"
// @Router /v1/user/login [post]
func (h *handler) Login(c *gin.Context) {
req := new(loginRequest)
@ -57,5 +57,5 @@ func (h *handler) Login(c *gin.Context) {
Version: version,
}
token, status := h.jwtService.SignClaim(claim)
e.Pong(c, status, loginResponse{Token: token, NickName: u.NickName})
e.Pong(c, status, LoginResponse{Token: token, NickName: u.NickName})
}

View File

@ -21,6 +21,10 @@ const (
TokenInvalid
TokenSignError
TokenRevoked
OAuthStateMismatch
OAuthExchangeFailed
OAuthVerifyFailed
OAuthGetClaimsFailed
)
const (
@ -30,6 +34,8 @@ const (
UserUnauthenticated
UserUnauthorized
UserDisabled
UserWithoutPassword
UserInvalid
)
const (
@ -81,6 +87,10 @@ var msgText = map[Status]string{
TokenInvalid: "Token Invalid",
TokenSignError: "Token Sign Error",
TokenRevoked: "Token Revoked",
OAuthStateMismatch: "OAuth State Mismatch",
OAuthExchangeFailed: "OAuth Exchange Failed",
OAuthVerifyFailed: "OAuth Verify Failed",
OAuthGetClaimsFailed: "OAuth Get Claims Failed",
UserNotFound: "User Not Found",
UserWrongPassword: "User Wrong Password",
@ -88,6 +98,8 @@ var msgText = map[Status]string{
UserUnauthenticated: "User Unauthenticated",
UserUnauthorized: "User Unauthorized",
UserDisabled: "User Disabled",
UserWithoutPassword: "User Without Password",
UserInvalid: "User Invalid",
ProblemNotFound: "Problem Not Found",
ProblemNotAvailable: "Problem Not Available",

View File

@ -9,6 +9,6 @@ type User struct {
UserName string `json:"user_name" gorm:"not null;uniqueIndex"`
NickName string `json:"nick_name" gorm:"not null"`
Role Role `json:"role" gorm:"not null"`
Password []byte `json:"-" gorm:"not null"`
Password []byte `json:"-"`
IsEnabled bool `json:"is_enabled" gorm:"not null;index"`
}

View File

@ -3,8 +3,12 @@ package model
type ConfigWebServer struct {
Address string `yaml:"Address"`
Port int `yaml:"Port"`
PublicBase string `yaml:"PublicBase"`
JwtSigningKey string `yaml:"JwtSigningKey"`
JwtExpireHour int `yaml:"JwtExpireHour"`
OAuthDomain string `yaml:"OAuthDomain"`
OAuthClientID string `yaml:"OAuthClientID"`
OAuthClientSecret string `yaml:"OAuthClientSecret"`
}
type ConfigRedis struct {

View File

@ -29,6 +29,10 @@ func (s *service) Login(data *LoginData) (*model.User, e.Status) {
if !user.IsEnabled {
return nil, e.UserDisabled
}
if len(user.Password) == 0 {
// created by oauth
return nil, e.UserWithoutPassword
}
err = bcrypt.CompareHashAndPassword(user.Password, []byte(data.Password))
if err != nil {

View File

@ -0,0 +1,103 @@
package oauth
import (
"context"
userApi "git.0x7f.app/WOJ/woj-server/internal/api/user"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
)
// CallbackHandler
// @Summary Callback with OAuth2
// @Description Callback endpoint from OAuth2
// @Tags oauth
// @Produce json
// @Router /oauth/callback [get]
func (s *service) CallbackHandler() gin.HandlerFunc {
// TODO: we are returning e.Response directly here, we should redirect to a trampoline page, passing the response as query string
return func(c *gin.Context) {
// verify state
signed, err := c.Cookie(oauthStateCookieName)
if err != nil {
e.Pong[any](c, e.InvalidParameter, nil)
return
}
state := c.Query("state")
if !utils.SignAndCompare(state, signed, []byte(s.conf.ClientSecret)) {
e.Pong[any](c, e.OAuthStateMismatch, nil)
return
}
// Exchange code for token
token, err := s.conf.Exchange(context.Background(), c.Query("code"))
if err != nil {
e.Pong[any](c, e.OAuthExchangeFailed, nil)
return
}
// Extract the ID Token from OAuth2 token.
raw, ok := token.Extra("id_token").(string)
if !ok {
e.Pong[any](c, e.OAuthExchangeFailed, nil)
return
}
// Parse and verify ID Token payload.
idToken, err := s.verifier.Verify(context.Background(), raw)
if err != nil {
e.Pong[any](c, e.OAuthVerifyFailed, nil)
return
}
// Extract custom claims
// TODO: extract role from claims
// TODO: currently username = email, add Email in User model
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Nickname string `json:"preferred_username"`
Role string `json:"role"`
}
if err := idToken.Claims(&claims); err != nil {
e.Pong[any](c, e.OAuthGetClaimsFailed, nil)
return
}
if !claims.EmailVerified || claims.Email == "" || claims.Nickname == "" {
e.Pong[any](c, e.UserInvalid, nil)
return
}
// Check User Existence
u, status := s.user.ProfileOrCreate(&user.CreateData{UserName: claims.Email, NickName: claims.Nickname})
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// Increment User Version
version, status := s.user.IncrVersion(u.ID)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// Sign JWT Token
claim := &model.Claim{
UID: u.ID,
Role: u.Role,
Version: version,
}
jwt, status := s.jwt.SignClaim(claim)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
e.Pong(c, status, userApi.LoginResponse{Token: jwt, NickName: u.NickName})
}
}

View File

@ -0,0 +1,32 @@
package oauth
import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
"net/http"
)
const (
oauthStateCookieName = "oauth_state"
)
// LoginHandler
// @Summary Login with OAuth2
// @Description Get OAuth2 Login URL
// @Tags oauth
// @Produce json
// @Response 200 {object} e.Response[string] "random string"
// @Router /oauth/login [post]
func (s *service) LoginHandler() gin.HandlerFunc {
return func(c *gin.Context) {
state := utils.RandomString(64)
signed := utils.SignString(state, []byte(s.conf.ClientSecret))
url := s.conf.AuthCodeURL(state)
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie(oauthStateCookieName, signed, 15*60, "/", "", false, true)
e.Pong(c, e.Success, url)
}
}

View File

@ -0,0 +1,92 @@
package oauth
import (
"context"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"go.uber.org/zap"
"golang.org/x/oauth2"
)
type Service interface {
LoginHandler() gin.HandlerFunc
CallbackHandler() gin.HandlerFunc
IsEnabled() bool
GetLoginPath() string
GetCallbackPath() string
HealthCheck() error
}
const (
basePath = "/oauth"
callbackPath = basePath + "/callback"
loginPath = basePath + "/login"
)
func NewService(i *do.Injector) (Service, error) {
srv := &service{}
srv.log = do.MustInvoke[log.Service](i).GetLogger("oauth")
srv.jwt = do.MustInvoke[jwt.Service](i)
srv.user = do.MustInvoke[user.Service](i)
srv.enabled = false
conf := do.MustInvoke[config.Service](i).GetConfig()
if conf.WebServer.OAuthDomain == "" {
return srv, srv.err
}
srv.provider, srv.err = oidc.NewProvider(context.Background(), conf.WebServer.OAuthDomain)
if srv.err != nil {
srv.log.Error("failed to create oauth provider", zap.Error(srv.err), zap.String("domain", conf.WebServer.OAuthDomain))
return srv, srv.err
}
srv.verifier = srv.provider.Verifier(&oidc.Config{ClientID: conf.WebServer.OAuthClientID})
srv.conf = oauth2.Config{
ClientID: conf.WebServer.OAuthClientID,
ClientSecret: conf.WebServer.OAuthClientSecret,
RedirectURL: conf.WebServer.PublicBase + callbackPath,
Endpoint: srv.provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email", "roles"},
}
srv.enabled = true
return srv, srv.err
}
type service struct {
log *zap.Logger
jwt jwt.Service
user user.Service
provider *oidc.Provider
conf oauth2.Config
verifier *oidc.IDTokenVerifier
enabled bool
err error
}
func (s *service) IsEnabled() bool {
return s.enabled && s.err == nil
}
func (s *service) GetLoginPath() string {
return loginPath
}
func (s *service) GetCallbackPath() string {
return callbackPath
}
func (s *service) HealthCheck() error {
return s.err
}

View File

@ -5,6 +5,7 @@ import (
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/web/metrics"
"git.0x7f.app/WOJ/woj-server/internal/web/oauth"
_ "git.0x7f.app/WOJ/woj-server/internal/web/router/docs"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
sentrygin "github.com/getsentry/sentry-go/gin"
@ -31,6 +32,7 @@ type Service interface {
func NewService(i *do.Injector) (Service, error) {
srv := &service{}
srv.metric = do.MustInvoke[metrics.Service](i)
srv.oauth = do.MustInvoke[oauth.Service](i)
srv.logger = do.MustInvoke[log.Service](i)
conf := do.MustInvoke[config.Service](i).GetConfig()
@ -40,9 +42,13 @@ func NewService(i *do.Injector) (Service, error) {
}
type service struct {
metric metrics.Service
logger log.Service
engine *gin.Engine
// middlewares
metric metrics.Service
oauth oauth.Service
err error
}
@ -129,6 +135,12 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En
api := r.Group("/api/")
s.setupApi(api, injector)
// oauth2
if s.oauth.IsEnabled() {
r.POST(s.oauth.GetLoginPath(), s.oauth.LoginHandler())
r.GET(s.oauth.GetCallbackPath(), s.oauth.CallbackHandler())
}
// fallback to frontend
r.NoRoute(func(c *gin.Context) {
c.File("./resource/frontend/index.html")

View File

@ -1,6 +1,9 @@
package utils
import (
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"math/rand"
)
@ -14,3 +17,22 @@ func RandomString(n int) string {
return string(s)
}
func SignString(s string, key []byte) string {
mac := hmac.New(sha512.New, key)
mac.Write([]byte(s))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func SignAndCompare(s string, exp string, key []byte) bool {
mac := hmac.New(sha512.New, key)
mac.Write([]byte(s))
decoded, err := base64.StdEncoding.DecodeString(exp)
if err != nil {
return false
}
return hmac.Equal(mac.Sum(nil), decoded)
}