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  }