diff --git a/Readme.md b/Readme.md index 36c7d61..6e924ef 100644 --- a/Readme.md +++ b/Readme.md @@ -14,4 +14,9 @@ | SMTP_PASSWORD | SMTP Password | `none` | | SMTP_FROM | Mail address used as from for all mails | `none` | | WEB_PUBLIC_REGISTRATION | Enabled the registration for anyone on the webgui | False | +| WEB_JWT_SECRET_KEY | Set secret key for the jwt used | `abc` | | MINIAUTH_LOGIN_WITH_NOT_APPROVED_MAIL | Allow user to login even if there mail address is not validated | False | + + +## Development +create locale db by run `sh db.sh` than set the env varieables you need and run everything. diff --git a/main.go b/main.go index 653cbdf..bdf62c1 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "html/template" "git.keks.cloud/kekskurse/miniauth/pkg/miniauth" + "git.keks.cloud/kekskurse/miniauth/pkg/oauthapi" "git.keks.cloud/kekskurse/miniauth/pkg/smtpclient" "git.keks.cloud/kekskurse/miniauth/pkg/userstore" "git.keks.cloud/kekskurse/miniauth/pkg/web" @@ -31,6 +32,7 @@ type gloableConfig struct { WebConfig web.WebConfig `env:", prefix=WEB_"` MiniauthConfig miniauth.MiniauthConfig `env:", prefix=MINIAUTH_"` DummyDatabase bool `env:"DUMMY_DATABASE"` + OauthConfig oauthapi.OauthAPIConf `env:", prefix=OAUTH_"` } func config() gloableConfig { @@ -79,6 +81,9 @@ func setupRouter(cfg gloableConfig) *gin.Engine { webServer := web.NewWeb(cfg.WebConfig, ma) webServer.RegisterRoutes(routesWeb) + oauthapi := oauthapi.NewOauthAPI(cfg.OauthConfig, ma) + oauthapi.RegisterRoutes(router.Group("/oauth/")) + router.GET("/", func(c *gin.Context) {}) router.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) diff --git a/pkg/oauthapi/migrations/1742764325_init.down.sql b/pkg/oauthapi/migrations/1742764325_init.down.sql new file mode 100644 index 0000000..234688e --- /dev/null +++ b/pkg/oauthapi/migrations/1742764325_init.down.sql @@ -0,0 +1 @@ +-- Write your down sql migration here \ No newline at end of file diff --git a/pkg/oauthapi/migrations/1742764325_init.up.sql b/pkg/oauthapi/migrations/1742764325_init.up.sql new file mode 100644 index 0000000..6e5638f --- /dev/null +++ b/pkg/oauthapi/migrations/1742764325_init.up.sql @@ -0,0 +1,12 @@ +-- Write your up sql migration here +CREATE TABLE clients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + client_id TEXT NOT NULL UNIQUE, + client_secret TEXT NOT NULL UNIQUE, + redirect_uris TEXT NOT NULL, + response_types TEXT NOT NULL, + grant_types TEXT NOT NULL, + scopes TEXT NOT NULL +) diff --git a/pkg/oauthapi/oauthapi.go b/pkg/oauthapi/oauthapi.go new file mode 100644 index 0000000..57f114a --- /dev/null +++ b/pkg/oauthapi/oauthapi.go @@ -0,0 +1,109 @@ +package oauthapi + +import ( + "crypto/rand" + "crypto/rsa" + "net/http" + "time" + + "git.keks.cloud/kekskurse/miniauth/pkg/miniauth" + "github.com/gin-gonic/gin" + "github.com/ory/fosite" + "github.com/ory/fosite/compose" + "github.com/ory/fosite/storage" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +type OauthAPIConf struct{} + +type OAuthAPI struct { + config OauthAPIConf + ma miniauth.Miniauth + log zerolog.Logger + oauth fosite.OAuth2Provider +} + +func NewOauthAPI(config OauthAPIConf, ma miniauth.Miniauth) OAuthAPI { + w := OAuthAPI{} + w.config = config + w.ma = ma + l := log.With().Str("pkg", "oauthapi").Logger() + w.log = l + + storage := storage.NewExampleStore() + secret := []byte("my super secret signing password") + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + w.log.Fatal().Err(err).Msg("cant create privatekey") + } + + oconfig := &fosite.Config{ + AccessTokenLifespan: time.Minute * 30, + GlobalSecret: secret, + } + + oauth2Provider := compose.ComposeAllEnabled(oconfig, storage, privateKey) + + w.oauth = oauth2Provider + return w +} + +func (w OAuthAPI) RegisterRoutes(routing *gin.RouterGroup) error { + routing.GET("/auth", w.authGet) + routing.POST("/auth", w.authPost) + routing.POST("/token", w.token) + return nil +} + +func (w OAuthAPI) authGet(ctx *gin.Context) { + ar, err := w.oauth.NewAuthorizeRequest(ctx, ctx.Request) + if err != nil { + log.Error().Err(err).Msg("cant create authorize request") + w.oauth.WriteAuthorizeError(ctx, ctx.Writer, ar, err) + return + } + w.log.Debug().Interface("ar", err).Msg("AuthorizeRequest") + + ctx.HTML(http.StatusOK, "login.html", nil) +} + +func (w OAuthAPI) authPost(ctx *gin.Context) { + ar, err := w.oauth.NewAuthorizeRequest(ctx, ctx.Request) + if err != nil { + log.Error().Err(err).Msg("cant create authorize request") + w.oauth.WriteAuthorizeError(ctx, ctx.Writer, ar, err) + return + } + + mySessionData := &fosite.DefaultSession{ + Username: ctx.PostForm("username"), + } + + response, err := w.oauth.NewAuthorizeResponse(ctx.Request.Context(), ar, mySessionData) + if err != nil { + log.Error().Err(err).Msg("cant create response") + w.oauth.WriteAuthorizeError(ctx, ctx.Writer, ar, err) + return + } + + w.oauth.WriteAuthorizeResponse(ctx, ctx.Writer, ar, response) +} + +func (w OAuthAPI) token(ctx *gin.Context) { + mySessionData := new(fosite.DefaultSession) + + accessRequest, err := w.oauth.NewAccessRequest(ctx, ctx.Request, mySessionData) + if err != nil { + w.oauth.WriteAccessError(ctx.Request.Context(), ctx.Writer, accessRequest, err) + return + } + + response, err := w.oauth.NewAccessResponse(ctx, accessRequest) + if err != nil { + w.oauth.WriteAccessError(ctx, ctx.Writer, accessRequest, err) + return + } + w.oauth.WriteAccessResponse(ctx, ctx.Writer, accessRequest, response) +} diff --git a/pkg/oauthapi/store.go b/pkg/oauthapi/store.go new file mode 100644 index 0000000..935104b --- /dev/null +++ b/pkg/oauthapi/store.go @@ -0,0 +1,10 @@ +package oauthapi + +import "github.com/rs/zerolog" + +type Config struct { + SQLite struct { + Path string `env:"SQLITE_PATH"` + } + Logger zerolog.Logger +} diff --git a/pkg/web/web.go b/pkg/web/web.go index e634225..f7a8371 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -3,13 +3,17 @@ package web import ( "errors" "net/http" + "time" "git.keks.cloud/kekskurse/miniauth/pkg/miniauth" + "git.keks.cloud/kekskurse/miniauth/pkg/userstore" "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" ) type WebConfig struct { - PublicRegistration bool `env:"PUBLIC_REGISTRATION, default=0"` + PublicRegistration bool `env:"PUBLIC_REGISTRATION, default=0"` + JWT_SECRET_KEY string `env:"JWT_SECRET_KEY, default=abc"` } type Web struct { @@ -68,11 +72,37 @@ func (w Web) PostLoginPage(c *gin.Context) { username := c.PostForm("username") password := c.PostForm("password") - err := w.ma.UserLogin(username, password) + _, err := w.ma.UserLogin(username, password) if err != nil { c.HTML(http.StatusOK, "login.html", gin.H{"msg": errors.Unwrap(err).Error()}) return } + c.SetSameSite(http.SameSiteStrictMode) + c.HTML(http.StatusOK, "msg.html", gin.H{"msg": "Login ok!"}) } + +func (w Web) DashboardPage(c *gin.Context) { +} + +type UserClaim struct { + User userstore.User + jwt.RegisteredClaims +} + +func (w Web) createToken(user userstore.User) (string, error) { + tokenLifespan := 15 * time.Minute + + claims := UserClaim{ + User: user, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "user-" + user.Username, + Issuer: "gin-jwt", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenLifespan)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return t.SignedString(w.config.JWT_SECRET_KEY) +} diff --git a/playwright/playwright.config.js b/playwright/playwright.config.js index acda745..deed741 100644 --- a/playwright/playwright.config.js +++ b/playwright/playwright.config.js @@ -31,6 +31,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + video: 'on', }, /* Configure projects for major browsers */