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 }