eintopf.info@v0.13.16/service/auth/transport.go (about) 1 // Copyright (C) 2022 The Eintopf authors 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <https://www.gnu.org/licenses/>. 15 16 package auth 17 18 import ( 19 "encoding/json" 20 "fmt" 21 "io" 22 "log" 23 "net/http" 24 25 "github.com/go-chi/chi/v5" 26 27 "eintopf.info/internal/xhttp" 28 ) 29 30 // Router returns a new auth router. 31 func Router(service Service) func(chi.Router) { 32 router := &router{service} 33 return func(r chi.Router) { 34 r.Options("/login", xhttp.CorsHandler) 35 36 // swagger:route POST /auth/login auth login 37 // 38 // Tries to log in the given user. 39 // 40 // When authentication was succesful returns a new JWT token. 41 // 42 // Responses: 43 // 200: loginSuccess 44 // 400: badRequest 45 // 500: internalError 46 // 503: serviceUnavailable 47 r.Post("/login", router.login) 48 } 49 } 50 51 type router struct { 52 s Service 53 } 54 55 func (router *router) login(w http.ResponseWriter, r *http.Request) { 56 req, err := readLoginRequest(r) 57 if err != nil { 58 xhttp.WriteInternalError(r.Context(), w, fmt.Errorf("failed to read login request: %s", err)) 59 return 60 } 61 token, err := router.s.Login(r.Context(), req.Credentials.Email, req.Credentials.Password) 62 if err == ErrInvalidCredentials { 63 xhttp.WriteBadRequest(r.Context(), w, err) 64 return 65 } 66 if err == ErrDeactivated { 67 xhttp.WriteForbidden(w, err) 68 return 69 } 70 if err != nil { 71 xhttp.WriteInternalError(r.Context(), w, fmt.Errorf("failed to login: %s", err)) 72 return 73 } 74 75 id, err := router.s.Authenticate(token) 76 if err != nil { 77 xhttp.WriteInternalError(r.Context(), w, fmt.Errorf("failed to login: %s", err)) 78 return 79 } 80 role, err := router.s.Authorize(r.Context(), id) 81 if err != nil { 82 xhttp.WriteInternalError(r.Context(), w, fmt.Errorf("failed to login: %s", err)) 83 return 84 } 85 err = writeLoginResponse(w, &loginResponse{LoginResponsePayload{ 86 Token: token, 87 ID: id, 88 Role: role, 89 }}) 90 if err != nil { 91 log.Println(fmt.Errorf("failed to write login response: %s", err)) 92 return 93 } 94 } 95 96 // swagger:parameters login 97 type loginRequest struct { 98 99 // in: body 100 Credentials Credentials 101 } 102 103 // Credentials define the login credentials. 104 type Credentials struct { 105 106 // required:true 107 Email string `json:"email"` 108 109 // required:true 110 Password string `json:"password"` 111 } 112 113 // swagger:response loginSuccess 114 type loginResponse struct { 115 // in:body 116 Body LoginResponsePayload 117 } 118 119 // LoginResponsePayload describes the payload of a succesful login xhttp. 120 type LoginResponsePayload struct { 121 Token string `json:"token"` 122 ID string `json:"id"` 123 Role Role `json:"role"` 124 } 125 126 func readLoginRequest(r *http.Request) (*loginRequest, error) { 127 data, err := io.ReadAll(r.Body) 128 if err != nil { 129 return nil, err 130 } 131 req := &loginRequest{} 132 err = json.Unmarshal(data, &req.Credentials) 133 if err != nil { 134 return nil, err 135 } 136 return req, nil 137 } 138 139 func writeLoginResponse(w http.ResponseWriter, resp *loginResponse) error { 140 data, err := json.Marshal(resp.Body) 141 if err != nil { 142 return err 143 } 144 145 w.Write(data) 146 return nil 147 } 148 149 // Middleware returns a new validating middleware. Calls MiddlewareWithOpts. 150 func Middleware(service Service) func(next http.Handler) http.Handler { 151 return MiddlewareWithOpts(service, MiddlewareOpts{Validate: true}) 152 } 153 154 // MiddlewareOpts are options used by the authorization middleware. 155 type MiddlewareOpts struct { 156 Validate bool 157 } 158 159 // MiddlewareWithOpts returns a new middleware, that adds user information from 160 // the "Authorization" header to the request context. If opts.Validate is set to 161 // true, it validates the user authorization and aborts for invalid 162 // authorization headers. 163 func MiddlewareWithOpts(service Service, opts MiddlewareOpts) func(next http.Handler) http.Handler { 164 return func(next http.Handler) http.Handler { 165 fn := func(w http.ResponseWriter, r *http.Request) { 166 token := r.Header.Get("Authorization") 167 if token == "" { 168 if opts.Validate { 169 xhttp.WriteUnauthorized(w, fmt.Errorf("missing authorization header")) 170 } else { 171 next.ServeHTTP(w, r.WithContext(r.Context())) 172 } 173 return 174 } 175 id, err := service.Authenticate(token) 176 if err != nil { 177 if opts.Validate { 178 xhttp.WriteInternalError(r.Context(), w, fmt.Errorf("failed to authenticate: %s", err)) 179 } else { 180 next.ServeHTTP(w, r.WithContext(r.Context())) 181 } 182 return 183 } 184 if id == "" { 185 if opts.Validate { 186 xhttp.WriteUnauthorized(w, fmt.Errorf("invalid authentication token")) 187 } else { 188 next.ServeHTTP(w, r.WithContext(r.Context())) 189 } 190 return 191 } 192 role, err := service.Authorize(r.Context(), id) 193 if err != nil { 194 if opts.Validate { 195 xhttp.WriteInternalError(r.Context(), w, fmt.Errorf("authorization error")) 196 } else { 197 next.ServeHTTP(w, r.WithContext(r.Context())) 198 } 199 return 200 } 201 ctx := r.Context() 202 ctx = ContextWithID(ctx, id) 203 ctx = ContextWithRole(ctx, role) 204 next.ServeHTTP(w, r.WithContext(ctx)) 205 } 206 return http.HandlerFunc(fn) 207 } 208 }