aboutsummaryrefslogtreecommitdiffstats
path: root/api/handlers
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2025-08-15 19:20:48 +0100
committerLeonardo Bishop <me@leonardobishop.com>2025-08-15 19:20:48 +0100
commit8f7dec8ba6b2f9bde01afd0a110596ebbd43e0ed (patch)
tree7b4f203d92f4b99b1e98fac314415e293984196b /api/handlers
parent4697556cac819c47d068819b9fc9c3b4ea84e279 (diff)
Implement OIDC
Diffstat (limited to 'api/handlers')
-rw-r--r--api/handlers/auth.go148
-rw-r--r--api/handlers/calendar.go2
-rw-r--r--api/handlers/users.go53
3 files changed, 160 insertions, 43 deletions
diff --git a/api/handlers/auth.go b/api/handlers/auth.go
new file mode 100644
index 0000000..c19fc3a
--- /dev/null
+++ b/api/handlers/auth.go
@@ -0,0 +1,148 @@
+package handlers
+
+import (
+ "errors"
+ "log/slog"
+ "net/http"
+
+ "github.com/LMBishop/confplanner/api/dto"
+ "github.com/LMBishop/confplanner/pkg/auth"
+ "github.com/LMBishop/confplanner/pkg/database/sqlc"
+ "github.com/LMBishop/confplanner/pkg/session"
+)
+
+func Login(authService auth.Service, store session.Service) http.HandlerFunc {
+ return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ provider := authService.GetAuthProvider(r.PathValue("provider"))
+
+ var user *sqlc.User
+ var err error
+ switch p := provider.(type) {
+ case *auth.BasicAuthProvider:
+ user, err = doBasicAuth(r, p)
+ case *auth.OIDCAuthProvider:
+ user, err = doOIDCAuthJourney(r, p)
+ default:
+ return &dto.ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: "Unknown auth provider",
+ }
+ }
+
+ if err != nil {
+ return err
+ }
+
+ // TODO X-Forwarded-For
+ session, err := store.Create(user.ID, user.Username, r.RemoteAddr, r.UserAgent())
+ if err != nil {
+ return err
+ }
+
+ cookie := &http.Cookie{
+ Name: "confplanner_session",
+ Value: session.Token,
+ Path: "/api",
+ }
+ http.SetCookie(w, cookie)
+
+ return &dto.OkResponse{
+ Code: http.StatusOK,
+ Data: &dto.LoginResponse{
+ ID: user.ID,
+ Username: user.Username,
+ },
+ }
+ })
+}
+
+func GetLoginOptions(authService auth.Service) http.HandlerFunc {
+ return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ var loginOptions []dto.LoginOption
+
+ for _, identifier := range authService.GetAuthProviders() {
+ provider := authService.GetAuthProvider(identifier)
+ loginOptions = append(loginOptions, dto.LoginOption{
+ Name: provider.Name(),
+ Identifier: identifier,
+ Type: provider.Type(),
+ })
+ }
+ return &dto.OkResponse{
+ Code: http.StatusOK,
+ Data: &dto.LoginOptionsResponse{
+ Options: loginOptions,
+ },
+ }
+ })
+}
+
+func doBasicAuth(r *http.Request, p *auth.BasicAuthProvider) (*sqlc.User, error) {
+ var request dto.LoginBasicRequest
+ if err := dto.ReadDto(r, &request); err != nil {
+ return nil, err
+ }
+
+ user, err := p.Authenticate(request.Username, request.Password)
+ if err != nil {
+ return nil, err
+ }
+
+ if user == nil {
+ return nil, &dto.ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: "Username and password combination not found",
+ }
+ }
+
+ return user, nil
+}
+
+func doOIDCAuthJourney(r *http.Request, p *auth.OIDCAuthProvider) (*sqlc.User, error) {
+ var request dto.LoginOAuthCallbackRequest
+ if err := dto.ReadDto(r, &request); err != nil {
+ url, err := p.StartJourney(r.RemoteAddr, r.UserAgent())
+ if err != nil {
+ return nil, &dto.ErrorResponse{
+ Code: http.StatusInternalServerError,
+ Message: "Could not start OAuth journey",
+ }
+ }
+
+ return nil, &dto.OkResponse{
+ Code: http.StatusTemporaryRedirect,
+ Data: &dto.LoginOAuthOutboundResponse{
+ URL: url,
+ },
+ }
+ }
+
+ user, err := p.CompleteJourney(r.Context(), request.Code, request.State, r.RemoteAddr, r.UserAgent())
+ if err != nil {
+ if errors.Is(err, auth.ErrNotAuthorised) {
+ return nil, &dto.ErrorResponse{
+ Code: http.StatusForbidden,
+ Message: "You are not authorised to use this service",
+ }
+ } else if errors.Is(err, auth.ErrInvalidState) {
+ return nil, &dto.ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: "Invalid state",
+ }
+ } else if errors.Is(err, auth.ErrStateVerificationFailed) {
+ return nil, &dto.ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: "State verification failed",
+ }
+ } else if errors.Is(err, auth.ErrUserSyncFailed) {
+ return nil, &dto.ErrorResponse{
+ Code: http.StatusInternalServerError,
+ Message: "User sync failed",
+ }
+ }
+ slog.Error("error completing oidc journey", "error", err, "ip", r.RemoteAddr)
+ return nil, err
+ }
+
+ return user, nil
+}
diff --git a/api/handlers/calendar.go b/api/handlers/calendar.go
index 5b14972..e0ed27f 100644
--- a/api/handlers/calendar.go
+++ b/api/handlers/calendar.go
@@ -32,7 +32,7 @@ func GetCalendar(calendarService calendar.Service, baseURL string) http.HandlerF
ID: cal.ID,
Name: cal.Name,
Key: cal.Key,
- URL: baseURL + "/calendar/ical?name=" + cal.Name + "&key=" + cal.Key,
+ URL: baseURL + "/api/calendar/ical?name=" + cal.Name + "&key=" + cal.Key,
},
}
})
diff --git a/api/handlers/users.go b/api/handlers/users.go
index efb2e29..3a1788d 100644
--- a/api/handlers/users.go
+++ b/api/handlers/users.go
@@ -5,18 +5,27 @@ import (
"net/http"
"github.com/LMBishop/confplanner/api/dto"
+ "github.com/LMBishop/confplanner/pkg/auth"
"github.com/LMBishop/confplanner/pkg/session"
"github.com/LMBishop/confplanner/pkg/user"
)
-func Register(service user.Service) http.HandlerFunc {
+func Register(userService user.Service, authService auth.Service) http.HandlerFunc {
return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
var request dto.RegisterRequest
if err := dto.ReadDto(r, &request); err != nil {
return err
}
- createdUser, err := service.CreateUser(request.Username, request.Password)
+ basicAuthProvider := authService.GetAuthProvider("basic")
+ if _, ok := basicAuthProvider.(*auth.BasicAuthProvider); !ok {
+ return &dto.ErrorResponse{
+ Code: http.StatusForbidden,
+ Message: "Registrations are only accepted via an identity provider",
+ }
+ }
+
+ createdUser, err := userService.CreateUser(request.Username, request.Password)
if err != nil {
if errors.Is(err, user.ErrUserExists) {
return &dto.ErrorResponse{
@@ -42,46 +51,6 @@ func Register(service user.Service) http.HandlerFunc {
})
}
-func Login(service user.Service, store session.Service) http.HandlerFunc {
- return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
- var request dto.LoginRequest
- if err := dto.ReadDto(r, &request); err != nil {
- return err
- }
-
- user, err := service.Authenticate(request.Username, request.Password)
- if err != nil {
- return err
- }
-
- if user == nil {
- return &dto.ErrorResponse{
- Code: http.StatusBadRequest,
- Message: "Username and password combination not found",
- }
- }
-
- session, err := store.Create(user.ID, user.Username, r.RemoteAddr, r.UserAgent())
- if err != nil {
- return err
- }
-
- cookie := &http.Cookie{
- Name: "confplanner_session",
- Value: session.Token,
- }
- http.SetCookie(w, cookie)
-
- return &dto.OkResponse{
- Code: http.StatusOK,
- Data: &dto.LoginResponse{
- ID: user.ID,
- Username: user.Username,
- },
- }
- })
-}
-
func Logout(store session.Service) http.HandlerFunc {
return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
session := r.Context().Value("session").(*session.UserSession)