features/jwt #7
2
go.mod
2
go.mod
@ -6,9 +6,11 @@ require (
|
|||||||
github.com/PuerkitoBio/goquery v1.8.0
|
github.com/PuerkitoBio/goquery v1.8.0
|
||||||
github.com/glebarez/go-sqlite v1.22.0
|
github.com/glebarez/go-sqlite v1.22.0
|
||||||
github.com/go-rod/rod v0.107.1
|
github.com/go-rod/rod v0.107.1
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/huandu/go-sqlbuilder v1.27.1
|
github.com/huandu/go-sqlbuilder v1.27.1
|
||||||
github.com/joho/godotenv v1.4.0
|
github.com/joho/godotenv v1.4.0
|
||||||
|
github.com/labstack/echo-jwt/v4 v4.2.0
|
||||||
github.com/labstack/echo/v4 v4.12.0
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
github.com/mmcdole/gofeed v1.1.3
|
github.com/mmcdole/gofeed v1.1.3
|
||||||
github.com/nicklaw5/helix/v2 v2.4.0
|
github.com/nicklaw5/helix/v2 v2.4.0
|
||||||
|
4
go.sum
4
go.sum
@ -35,6 +35,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
|
|||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
@ -60,6 +62,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
|
||||||
|
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
|
||||||
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
@ -3,7 +3,10 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
echojwt "github.com/labstack/echo-jwt/v4"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
swagger "github.com/swaggo/echo-swagger"
|
swagger "github.com/swaggo/echo-swagger"
|
||||||
@ -17,7 +20,6 @@ import (
|
|||||||
type Handler struct {
|
type Handler struct {
|
||||||
Router *echo.Echo
|
Router *echo.Echo
|
||||||
Db *database.Queries
|
Db *database.Queries
|
||||||
//dto *dto.DtoClient
|
|
||||||
config services.Configs
|
config services.Configs
|
||||||
repo services.RepositoryService
|
repo services.RepositoryService
|
||||||
}
|
}
|
||||||
@ -28,6 +30,7 @@ const (
|
|||||||
ErrUnableToParseId = "Unable to parse the requested ID"
|
ErrUnableToParseId = "Unable to parse the requested ID"
|
||||||
ErrRecordMissing = "The requested record was not found"
|
ErrRecordMissing = "The requested record was not found"
|
||||||
ErrFailedToCreateRecord = "The record was not created due to a database problem"
|
ErrFailedToCreateRecord = "The record was not created due to a database problem"
|
||||||
|
ErrUserUnknown = "User is unknown"
|
||||||
|
|
||||||
ResponseMessageSuccess = "Success"
|
ResponseMessageSuccess = "Success"
|
||||||
)
|
)
|
||||||
@ -47,6 +50,13 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han
|
|||||||
repo: services.NewRepositoryService(conn),
|
repo: services.NewRepositoryService(conn),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jwtConfig := echojwt.Config{
|
||||||
|
NewClaimsFunc: func(c echo.Context) jwt.Claims {
|
||||||
|
return new(JwtToken)
|
||||||
|
},
|
||||||
|
SigningKey: []byte(configs.JwtSecret),
|
||||||
|
}
|
||||||
|
|
||||||
router := echo.New()
|
router := echo.New()
|
||||||
router.Pre(middleware.RemoveTrailingSlash())
|
router.Pre(middleware.RemoveTrailingSlash())
|
||||||
router.Pre(middleware.Logger())
|
router.Pre(middleware.Logger())
|
||||||
@ -55,6 +65,7 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han
|
|||||||
|
|
||||||
v1 := router.Group("/api/v1")
|
v1 := router.Group("/api/v1")
|
||||||
articles := v1.Group("/articles")
|
articles := v1.Group("/articles")
|
||||||
|
articles.Use(echojwt.WithConfig(jwtConfig))
|
||||||
articles.GET("", s.listArticles)
|
articles.GET("", s.listArticles)
|
||||||
articles.GET(":id", s.getArticle)
|
articles.GET(":id", s.getArticle)
|
||||||
articles.GET(":id/details", s.getArticleDetails)
|
articles.GET(":id/details", s.getArticleDetails)
|
||||||
@ -76,6 +87,7 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han
|
|||||||
//settings.GET("/", s.getSettings)
|
//settings.GET("/", s.getSettings)
|
||||||
|
|
||||||
sources := v1.Group("/sources")
|
sources := v1.Group("/sources")
|
||||||
|
sources.Use(echojwt.WithConfig(jwtConfig))
|
||||||
sources.GET("", s.listSources)
|
sources.GET("", s.listSources)
|
||||||
sources.GET("/by/source", s.listSourcesBySource)
|
sources.GET("/by/source", s.listSourcesBySource)
|
||||||
sources.GET("/by/sourceAndName", s.GetSourceBySourceAndName)
|
sources.GET("/by/sourceAndName", s.GetSourceBySourceAndName)
|
||||||
@ -89,6 +101,7 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han
|
|||||||
sources.POST("/:ID/enable", s.enableSource)
|
sources.POST("/:ID/enable", s.enableSource)
|
||||||
|
|
||||||
subs := v1.Group("/subscriptions")
|
subs := v1.Group("/subscriptions")
|
||||||
|
subs.Use(echojwt.WithConfig(jwtConfig))
|
||||||
subs.GET("/", s.ListSubscriptions)
|
subs.GET("/", s.ListSubscriptions)
|
||||||
subs.GET("/details", s.ListSubscriptionDetails)
|
subs.GET("/details", s.ListSubscriptionDetails)
|
||||||
subs.GET("/by/discordId", s.GetSubscriptionsByDiscordId)
|
subs.GET("/by/discordId", s.GetSubscriptionsByDiscordId)
|
||||||
@ -120,3 +133,19 @@ func (s *Handler) WriteMessage(c echo.Context, msg string, HttpStatusCode int) e
|
|||||||
Message: msg,
|
Message: msg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) {
|
||||||
|
// Make sure that the request came with a jwtToken
|
||||||
|
token, ok := c.Get("user").(*jwt.Token)
|
||||||
|
if !ok {
|
||||||
|
return JwtToken{}, errors.New(ErrJwtMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the claims from the token
|
||||||
|
claims, ok := token.Claims.(*JwtToken)
|
||||||
|
if !ok {
|
||||||
|
return JwtToken{}, errors.New(ErrJwtClaimsMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *claims, nil
|
||||||
|
}
|
||||||
|
99
internal/handler/v1/jwt.go
Normal file
99
internal/handler/v1/jwt.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrJwtMissing = "auth token is missing"
|
||||||
|
ErrJwtClaimsMissing = "claims missing on token"
|
||||||
|
ErrJwtExpired = "auth token has expired"
|
||||||
|
ErrJwtScopeMissing = "required scope is missing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JwtToken struct {
|
||||||
|
Exp time.Time `json:"exp"`
|
||||||
|
Iss string `json:"iss"`
|
||||||
|
Authorized bool `json:"authorized"`
|
||||||
|
UserName string `json:"username"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j JwtToken) IsValid(scope string) error {
|
||||||
|
err := j.hasExpired()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = j.hasScope(scope)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j JwtToken) GetUsername() string {
|
||||||
|
return j.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j JwtToken) hasExpired() error {
|
||||||
|
// Check to see if the token has expired
|
||||||
|
hasExpired := j.Exp.Compare(time.Now())
|
||||||
|
if hasExpired == -1 {
|
||||||
|
return errors.New(ErrJwtExpired)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j JwtToken) hasScope(scope string) error {
|
||||||
|
// they have the scope to access everything, so let them pass.
|
||||||
|
if strings.Contains(domain.ScopeAll, scope) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range j.Scopes {
|
||||||
|
if strings.Contains(s, scope) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New(ErrJwtScopeMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) generateJwt(username, issuer string) (string, error) {
|
||||||
|
return h.generateJwtWithExp(username, issuer, time.Now().Add(10*time.Minute))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Time) (string, error) {
|
||||||
|
secret := []byte(h.config.JwtSecret)
|
||||||
|
|
||||||
|
// Anyone who wants to decrypt the key needs to use the same method
|
||||||
|
token := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["exp"] = expiresAt
|
||||||
|
claims["authorized"] = true
|
||||||
|
claims["username"] = username
|
||||||
|
claims["iss"] = issuer
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if username == "admin" {
|
||||||
|
scopes = append(scopes, domain.ScopeAll)
|
||||||
|
claims["scopes"] = scopes
|
||||||
|
} else {
|
||||||
|
scopes = append(scopes, domain.ScopeRead)
|
||||||
|
claims["scopes"] = scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString, err := token.SignedString(secret)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
@ -34,6 +34,7 @@ const (
|
|||||||
|
|
||||||
type Configs struct {
|
type Configs struct {
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
|
JwtSecret string
|
||||||
|
|
||||||
RedditEnabled bool
|
RedditEnabled bool
|
||||||
RedditPullTop bool
|
RedditPullTop bool
|
||||||
@ -64,6 +65,7 @@ func NewConfig() ConfigClient {
|
|||||||
func GetEnvConfig() Configs {
|
func GetEnvConfig() Configs {
|
||||||
return Configs{
|
return Configs{
|
||||||
ServerAddress: os.Getenv(ServerAddress),
|
ServerAddress: os.Getenv(ServerAddress),
|
||||||
|
JwtSecret: os.Getenv("JwtSecret"),
|
||||||
|
|
||||||
RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)),
|
RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)),
|
||||||
RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)),
|
RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)),
|
||||||
|
1
makefile
1
makefile
@ -6,6 +6,7 @@ build: ## builds the application with the current go runtime
|
|||||||
~/go/bin/swag f
|
~/go/bin/swag f
|
||||||
~/go/bin/swag init -g cmd/server.go
|
~/go/bin/swag init -g cmd/server.go
|
||||||
go build cmd/server.go
|
go build cmd/server.go
|
||||||
|
ls -lh server
|
||||||
|
|
||||||
docker-build: ## Generates the docker image
|
docker-build: ## Generates the docker image
|
||||||
docker build -t "newsbot.collector.api" .
|
docker build -t "newsbot.collector.api" .
|
||||||
|
Loading…
Reference in New Issue
Block a user