package authentication import ( "context" "ersteller-lib" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" "github.com/markbates/goth" "github.com/markbates/goth/gothic" "github.com/markbates/goth/providers/openidConnect" ) const OpenIdConnectPath = "/auth/openid-connect" type KeycloakEnvClient struct { ClientId string ClientSecret string DiscoveryUrl string } type KeycloakEnv struct { SessionSecret string BaseUrl string SessionName string Keycloak KeycloakEnvClient EmailSessionKey string UserIdSessionKey string } // https://keycloak-dev.deploy.ersteller.gorlug.de/realms/master/.well-known/openid-configuration // https://go.dev/play/p/-RtLSPL4Wsj func RunKeycloakAuth(e *echo.Echo, environment KeycloakEnv, cookieStore *sessions.CookieStore, userRepo UserRepository) { sessionStore := sessions.NewFilesystemStore("store", []byte(environment.SessionSecret)) sessionStore.MaxLength(8192) gothic.Store = sessionStore // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html) // because the OpenID Connect provider initialize itself in the New(), it can return an error which should be handled or ignored // ignore the error for now openid, err := openidConnect.New(environment.Keycloak.CLientId, environment.Keycloak.ClientSecret, environment.BaseUrl+"/auth/openid-connect/callback", environment.Keycloak.DiscoveryUrl) if err != nil { ersteller_lib.Error("Error while initializing OpenID Connect provider: ", err) panic(err) } if openid != nil { goth.UseProviders(openid) } e.GET("/auth/openid-connect/callback", func(c echo.Context) error { user, err := gothic.CompleteUserAuth(c.Response(), c.Request()) if err != nil { return err } userId, err := userRepo.GetUserId(user.Email) if err != nil { if userId == -1 { userId, err = createUser(user, userRepo) if err != nil { ersteller_lib.LogError("Failed to create user: %v", err) return err } ersteller_lib.LogDebug("Created user with id %d", userId) } else { ersteller_lib.LogError("Failed to get user id: %v", err) return err } } err = saveEmailToSessionStore(c, cookieStore, user.Email, userId, environment) if err != nil { return err } return c.Redirect(302, "/") }) e.GET("/logout", func(c echo.Context) error { // Get the session session, err := cookieStore.Get(c.Request(), environment.SessionName) if err != nil { ersteller_lib.LogError("Failed to get session during logout: %v", err) } else { // Clear session values session.Values = make(map[interface{}]interface{}) // Set MaxAge to -1 to delete the cookie session.Options.MaxAge = -1 // Save the session (this will delete the cookie) err = session.Save(c.Request(), c.Response()) if err != nil { ersteller_lib.LogError("Failed to clear session during logout: %v", err) } } // Also call gothic logout for OpenID Connect cleanup gothic.Logout(c.Response(), c.Request()) // Redirect to login page return c.Redirect(302, "/login") }) e.GET("/logout/openid-connect", func(c echo.Context) error { return gothic.Logout(c.Response(), c.Request()) }) e.GET(OpenIdConnectPath, func(c echo.Context) error { ctx := context.WithValue(c.Request().Context(), gothic.ProviderParamKey, "openid-connect") request := c.Request().WithContext(ctx) // try to get the user without re-authenticating if gothUser, err := gothic.CompleteUserAuth(c.Response(), c.Request()); err == nil { ersteller_lib.Debug(gothUser) return nil } else { gothic.BeginAuthHandler(c.Response(), request) return nil } }) } func createUser(gothUser goth.User, repo UserRepository) (int, error) { return repo.CreateFromEmail(gothUser.Email) } func saveEmailToSessionStore(c echo.Context, sessionStore *sessions.CookieStore, email string, userId int, environment KeycloakEnv) error { session, err := sessionStore.New(c.Request(), environment.SessionName) if err != nil { ersteller_lib.LogError("Failed to create session: %v", err) return err } session.Values = map[interface{}]interface{}{ environment.EmailSessionKey: email, environment.UserIdSessionKey: userId, } err = session.Save(c.Request(), c.Response()) if err != nil { ersteller_lib.LogError("Failed to save session: %v", err) return err } return nil }