github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/auth.go (about)

     1  /*
     2   * Copyright (C) 2019 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package endpoints
    19  
    20  import (
    21  	"encoding/json"
    22  	"net/http"
    23  	"net/url"
    24  	"time"
    25  
    26  	"github.com/gin-gonic/gin"
    27  	"github.com/rs/zerolog/log"
    28  
    29  	"github.com/mysteriumnetwork/go-rest/apierror"
    30  	"github.com/mysteriumnetwork/node/core/auth"
    31  	"github.com/mysteriumnetwork/node/tequilapi/contract"
    32  	"github.com/mysteriumnetwork/node/tequilapi/sso"
    33  	"github.com/mysteriumnetwork/node/tequilapi/utils"
    34  )
    35  
    36  type authenticationAPI struct {
    37  	jwtAuthenticator jwtAuthenticator
    38  	authenticator    authenticator
    39  	ssoMystnodes     *sso.Mystnodes
    40  }
    41  
    42  type jwtAuthenticator interface {
    43  	CreateToken(username string) (auth.JWT, error)
    44  }
    45  
    46  type authenticator interface {
    47  	CheckCredentials(username, password string) error
    48  	ChangePassword(username, oldPassword, newPassword string) error
    49  }
    50  
    51  // swagger:operation POST /auth/authenticate Authentication Authenticate
    52  //
    53  //	---
    54  //	summary: Authenticate
    55  //	description: Authenticates user and issues auth token
    56  //	parameters:
    57  //	  - in: body
    58  //	    name: body
    59  //	    schema:
    60  //	      $ref: "#/definitions/AuthRequest"
    61  //	responses:
    62  //	  200:
    63  //	    description: Authentication succeeded
    64  //	    schema:
    65  //	      "$ref": "#/definitions/AuthResponse"
    66  //	  400:
    67  //	    description: Failed to parse or request validation failed
    68  //	    schema:
    69  //	      "$ref": "#/definitions/APIError"
    70  //	  401:
    71  //	    description: Authentication failed
    72  //	    schema:
    73  //	      "$ref": "#/definitions/APIError"
    74  func (api *authenticationAPI) Authenticate(c *gin.Context) {
    75  	req, err := toAuthRequest(c.Request)
    76  	if err != nil {
    77  		c.Error(apierror.ParseFailed())
    78  		return
    79  	}
    80  	err = api.authenticator.CheckCredentials(req.Username, req.Password)
    81  	if err != nil {
    82  		c.Error(apierror.Unauthorized())
    83  		return
    84  	}
    85  
    86  	jwtToken, err := api.jwtAuthenticator.CreateToken(req.Username)
    87  	if err != nil {
    88  		c.Error(apierror.Unauthorized())
    89  		return
    90  	}
    91  
    92  	response := contract.NewAuthResponse(jwtToken)
    93  	utils.WriteAsJSON(response, c.Writer)
    94  }
    95  
    96  // swagger:operation POST /auth/login Authentication Login
    97  //
    98  //	---
    99  //	summary: Login
   100  //	description: Authenticates user and sets cookie with issued auth token
   101  //	parameters:
   102  //	  - in: body
   103  //	    name: body
   104  //	    schema:
   105  //	      $ref: "#/definitions/AuthRequest"
   106  //	responses:
   107  //	  200:
   108  //	    description: Authentication succeeded
   109  //	    schema:
   110  //	      "$ref": "#/definitions/AuthResponse"
   111  //	  400:
   112  //	    description: Failed to parse or request validation failed
   113  //	    schema:
   114  //	      "$ref": "#/definitions/APIError"
   115  //	  401:
   116  //	    description: Authentication failed
   117  //	    schema:
   118  //	      "$ref": "#/definitions/APIError"
   119  func (api *authenticationAPI) Login(c *gin.Context) {
   120  	req, err := toAuthRequest(c.Request)
   121  	if err != nil {
   122  		c.Error(apierror.ParseFailed())
   123  		return
   124  	}
   125  	err = api.authenticator.CheckCredentials(req.Username, req.Password)
   126  	if err != nil {
   127  		c.Error(apierror.Unauthorized())
   128  		return
   129  	}
   130  
   131  	jwtToken, err := api.jwtAuthenticator.CreateToken(req.Username)
   132  	if err != nil {
   133  		c.Error(apierror.Unauthorized())
   134  		return
   135  	}
   136  
   137  	response := contract.NewAuthResponse(jwtToken)
   138  
   139  	http.SetCookie(c.Writer, &http.Cookie{
   140  		Name:     auth.JWTCookieName,
   141  		Value:    jwtToken.Token,
   142  		Expires:  jwtToken.ExpirationTime,
   143  		HttpOnly: true,
   144  		Secure:   false,
   145  		Path:     "/",
   146  	})
   147  	utils.WriteAsJSON(response, c.Writer)
   148  }
   149  
   150  // swagger:operation GET /auth/login-mystnodes SSO LoginMystnodesInit
   151  //
   152  //	---
   153  //	summary: LoginMystnodesInit
   154  //	description: SSO init endpoint to auth via mystnodes
   155  //	parameters:
   156  //	  - in: query
   157  //	    name: redirect_url
   158  //	    description: a redirect to send authorization grant to
   159  //	    type: string
   160  //
   161  //	responses:
   162  //	  200:
   163  //	    description: link response
   164  //	    schema:
   165  //	      "$ref": "#/definitions/MystnodesSSOLinkResponse"
   166  func (api *authenticationAPI) LoginMystnodesInit(c *gin.Context) {
   167  	redirectURL, err := url.Parse(c.Query("redirect_uri"))
   168  	if err != nil {
   169  		log.Err(err).Msg("failed to generate mystnodes SSO link")
   170  		c.Error(apierror.Unauthorized())
   171  		return
   172  	}
   173  
   174  	link, err := api.ssoMystnodes.SSOLink(redirectURL)
   175  	if err != nil {
   176  		log.Err(err).Msg("failed to generate mystnodes SSO link")
   177  		c.Error(apierror.Unauthorized())
   178  		return
   179  	}
   180  	c.JSON(http.StatusOK, contract.MystnodesSSOLinkResponse{Link: link.String()})
   181  }
   182  
   183  // swagger:operation POST /auth/login-mystnodes SSO LoginMystnodesWithGrant
   184  //
   185  //	---
   186  //	summary: LoginMystnodesWithGrant
   187  //	description: SSO login using grant provided by mystnodes.com
   188  //
   189  //	responses:
   190  //	  200:
   191  //	    description: grant was verified against mystnodes using PKCE workflow. This will set access token cookie.
   192  //	  401:
   193  //	    description: grant failed to be verified
   194  func (api *authenticationAPI) LoginMystnodesWithGrant(c *gin.Context) {
   195  	var request contract.MystnodesSSOGrantLoginRequest
   196  	err := json.NewDecoder(c.Request.Body).Decode(&request)
   197  	if err != nil {
   198  		log.Err(err).Msg("failed decoding request")
   199  		c.Error(apierror.Unauthorized())
   200  		return
   201  	}
   202  
   203  	_, err = api.ssoMystnodes.VerifyAuthorizationGrant(request.AuthorizationGrant, sso.DefaultVerificationOptions)
   204  	if err != nil {
   205  		log.Err(err).Msg("failed to verify mystnodes Authorization Grant")
   206  		c.Error(apierror.Unauthorized())
   207  		return
   208  	}
   209  
   210  	jwtToken, err := api.jwtAuthenticator.CreateToken("myst")
   211  	if err != nil {
   212  		c.Error(apierror.Unauthorized())
   213  		return
   214  	}
   215  
   216  	response := contract.NewAuthResponse(jwtToken)
   217  
   218  	http.SetCookie(c.Writer, &http.Cookie{
   219  		Name:     auth.JWTCookieName,
   220  		Value:    jwtToken.Token,
   221  		Expires:  jwtToken.ExpirationTime,
   222  		HttpOnly: true,
   223  		Secure:   false,
   224  		Path:     "/",
   225  	})
   226  	utils.WriteAsJSON(response, c.Writer)
   227  }
   228  
   229  // swagger:operation DELETE /auth/logout Authentication Logout
   230  //
   231  //	---
   232  //	summary: Logout
   233  //	description: Clears authentication cookie
   234  //	responses:
   235  //	  200:
   236  //	    description: Logged out successfully
   237  func (api *authenticationAPI) Logout(c *gin.Context) {
   238  	http.SetCookie(c.Writer, &http.Cookie{
   239  		Name:     auth.JWTCookieName,
   240  		Value:    "",
   241  		Expires:  time.Unix(0, 0),
   242  		MaxAge:   0,
   243  		HttpOnly: true,
   244  		Secure:   false,
   245  		Path:     "/",
   246  	})
   247  }
   248  
   249  // swagger:operation PUT /auth/password Authentication changePassword
   250  //
   251  //	---
   252  //	summary: Change password
   253  //	description: Changes user password
   254  //	parameters:
   255  //	  - in: body
   256  //	    name: body
   257  //	    schema:
   258  //	      $ref: "#/definitions/ChangePasswordRequest"
   259  //	responses:
   260  //	  200:
   261  //	    description: Password changed successfully
   262  //	  400:
   263  //	    description: Failed to parse or request validation failed
   264  //	    schema:
   265  //	      "$ref": "#/definitions/APIError"
   266  //	  401:
   267  //	    description: Unauthorized
   268  //	    schema:
   269  //	      "$ref": "#/definitions/APIError"
   270  func (api *authenticationAPI) ChangePassword(c *gin.Context) {
   271  	var req *contract.ChangePasswordRequest
   272  	var err error
   273  	req, err = toChangePasswordRequest(c.Request)
   274  	if err != nil {
   275  		c.Error(apierror.ParseFailed())
   276  		return
   277  	}
   278  	err = api.authenticator.ChangePassword(req.Username, req.OldPassword, req.NewPassword)
   279  	if err != nil {
   280  		c.Error(apierror.Unauthorized())
   281  		return
   282  	}
   283  }
   284  
   285  func toAuthRequest(req *http.Request) (contract.AuthRequest, error) {
   286  	var request contract.AuthRequest
   287  	err := json.NewDecoder(req.Body).Decode(&request)
   288  	return request, err
   289  }
   290  
   291  func toChangePasswordRequest(req *http.Request) (*contract.ChangePasswordRequest, error) {
   292  	var cpReq = contract.ChangePasswordRequest{}
   293  	if err := json.NewDecoder(req.Body).Decode(&cpReq); err != nil {
   294  		return nil, err
   295  	}
   296  	return &cpReq, nil
   297  }
   298  
   299  // AddRoutesForAuthentication registers /auth endpoints in Tequilapi
   300  func AddRoutesForAuthentication(auth authenticator, jwtAuth jwtAuthenticator, ssoMystnodes *sso.Mystnodes) func(*gin.Engine) error {
   301  	api := &authenticationAPI{
   302  		authenticator:    auth,
   303  		jwtAuthenticator: jwtAuth,
   304  		ssoMystnodes:     ssoMystnodes,
   305  	}
   306  	return func(e *gin.Engine) error {
   307  		g := e.Group("/auth")
   308  		{
   309  			g.PUT("/password", api.ChangePassword)
   310  			g.POST("/authenticate", api.Authenticate)
   311  			g.POST("/login", api.Login)
   312  			g.GET("/login-mystnodes", api.LoginMystnodesInit)
   313  			g.POST("/login-mystnodes", api.LoginMystnodesWithGrant)
   314  			g.DELETE("/logout", api.Logout)
   315  		}
   316  		return nil
   317  	}
   318  }