diff options
Diffstat (limited to 'api/handlers')
| -rw-r--r-- | api/handlers/auth.go | 148 | ||||
| -rw-r--r-- | api/handlers/calendar.go | 2 | ||||
| -rw-r--r-- | api/handlers/users.go | 53 |
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) |
