cmd/hub/api/login.go (223 lines of code) (raw):
// Copyright (c) 2022 EPAM Systems, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build api
package api
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/epam/hubctl/cmd/hub/config"
"github.com/epam/hubctl/cmd/hub/util"
)
type AuthUserPass struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginTokenResponse struct {
LoginToken string `json:"loginToken"`
}
type AuthLoginToken struct {
LoginToken string `json:"loginToken"`
}
type SigninResponse struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
Exp int64 `json:"exp,omitempty"`
}
type AuthPingResponse struct {
Exp int64 `json:"exp"`
}
const loginTokenResource = "auth/api/v1/users/credentials/login-token"
const signinResource = "auth/api/v1/signin"
const authPingResource = "auth/api/v1/authenticated-ping"
const refreshResource = "auth/api/v1/refresh"
var accessTokenExpectedLifetime = 600 * time.Second
func Login(apiBaseUrl, username, password string) {
token, err := loginForToken(apiBaseUrl, username, password)
if err != nil {
log.Fatalf("Unable to login: %v", err)
}
fmt.Printf(`# eval this in your shell
export HUB_API=%s
export HUB_TOKEN=%s
`, apiBaseUrl, token)
}
func loginForToken(apiBaseUrl, username, password string) (string, error) {
reqBody, err := json.Marshal(&AuthUserPass{Username: username, Password: password})
if err != nil {
return "", fmt.Errorf("Error marshalling sign-in request: %v", err)
}
path := fmt.Sprintf("%s/%s", apiBaseUrl, loginTokenResource)
if config.Trace {
log.Printf(">>> POST %s\n%s", path, identJson(reqBody))
}
req, err := http.NewRequest("POST", path, bytes.NewReader(reqBody))
if err != nil {
return "", fmt.Errorf("Error creating Auth Service sign-in request: %v", err)
}
req.Header.Add("Content-type", "application/json")
var jsResp LoginTokenResponse
code, _, err := do(hubApi(), req, &jsResp)
if code == 404 {
return "", fmt.Errorf("No user found (404 HTTP)")
}
if err != nil {
return "", fmt.Errorf("Sign-in error: %v", err)
}
if code != 200 {
return "", fmt.Errorf("Got %d HTTP from Auth Service sign-in, expected 200 HTTP", code)
}
if jsResp.LoginToken == "" {
return "", fmt.Errorf("Empty or no `loginToken` in sign-in response")
}
return jsResp.LoginToken, nil
}
func loginWithToken(apiBaseUrl, token string) (*SigninResponse, error) {
reqBody, err := json.Marshal(&AuthLoginToken{token})
if err != nil {
return nil, fmt.Errorf("Error marshalling sign-in request: %v", err)
}
path := fmt.Sprintf("%s/%s", apiBaseUrl, signinResource)
if config.Trace {
log.Printf(">>> POST %s\n%s", path, identJson(reqBody))
}
req, err := http.NewRequest("POST", path, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("Error creating Auth Service sign-in request: %v", err)
}
req.Header.Add("Content-type", "application/json")
var jsResp SigninResponse
code, _, err := do(hubApi(), req, &jsResp)
if code == 404 {
return nil, fmt.Errorf("No user found (404 HTTP)")
}
if err != nil {
return nil, fmt.Errorf("Sign-in error: %v", err)
}
if code != 200 {
return nil, fmt.Errorf("Got %d HTTP from Auth Service sign-in, expected 200 HTTP", code)
}
if jsResp.AccessToken == "" {
return nil, fmt.Errorf("Empty or no `accessToken` in sign-in response")
}
return &jsResp, nil
}
var hubApiBearerToken string
var hubApiBearerTokenExpTime time.Time
func tokenTimeValid(tokenTime time.Time) bool {
return tokenTime.After(time.Now().Add(accessTokenExpectedLifetime))
}
func bearerToken() string {
if hubApiBearerToken != "" && tokenTimeValid(hubApiBearerTokenExpTime) {
return hubApiBearerToken
}
if config.ApiLoginToken == "" {
log.Fatalf("Login token is not supplied - use `hub login` to obtain one")
}
cachedTokens, err := loadAccessToken(config.ApiBaseUrl, config.ApiLoginToken)
if err != nil {
util.Warn("Unable to load cached API access token (requesting new): %v", err)
}
if cachedTokens != nil {
code, resp, err := verifyAccessToken(cachedTokens.AccessToken)
if err != nil {
if code != 401 {
util.Warn("Unable to verify API access token (requesting new): %v", err)
}
} else {
if code == 200 {
if resp != nil {
exp := time.Unix(resp.Exp, 0)
if config.Debug {
log.Printf("API access token verified; expiry at %v", exp)
}
if tokenTimeValid(exp) {
hubApiBearerToken = cachedTokens.AccessToken
hubApiBearerTokenExpTime = exp
return hubApiBearerToken
}
if config.Debug {
log.Print("API access token must be refreshed")
}
}
} else if code != 401 {
util.Warn("Got %d HTTP while verifying API access token - expected 401 HTTP (requesting new)", code)
}
resp, err := refreshAccessToken(cachedTokens)
if err != nil {
util.Warn("Unable to refresh API access token (requesting new): %v", err)
}
if resp != nil {
exp := time.Unix(resp.Exp, 0)
if config.Debug {
log.Printf("API access token refreshed; expiry at %v", exp)
}
if !tokenTimeValid(exp) {
util.Warn("API refreshed token expiry too short %v", exp)
}
storeAccessToken(config.ApiBaseUrl, config.ApiLoginToken, resp)
if err != nil {
util.Warn("Unable to store API access token in cache: %v", err)
}
hubApiBearerToken = resp.AccessToken
hubApiBearerTokenExpTime = exp
return hubApiBearerToken
}
}
}
resp, err := loginWithToken(config.ApiBaseUrl, config.ApiLoginToken)
if err != nil {
log.Fatalf("Unable to login with token: %v", err)
}
exp := time.Unix(resp.Exp, 0)
if config.Debug {
log.Printf("New API access token obtained; expiry at %v", exp)
}
if !tokenTimeValid(exp) {
util.Warn("API token expiry too short %v", exp)
}
storeAccessToken(config.ApiBaseUrl, config.ApiLoginToken, resp)
if err != nil {
util.Warn("Unable to store API access token in cache: %v", err)
}
hubApiBearerToken = resp.AccessToken
hubApiBearerTokenExpTime = exp
return hubApiBearerToken
}
func verifyAccessToken(accessToken string) (int, *AuthPingResponse, error) {
req, err := hubRequest("GET", authPingResource, accessToken, nil)
if err != nil {
return 0, nil, err
}
var jsResp AuthPingResponse
code, _, err := do(hubApi(), req, &jsResp)
if err != nil {
return code, nil, err
}
return code, &jsResp, nil
}
func refreshAccessToken(tokens *SigninResponse) (*SigninResponse, error) {
reqBody, err := json.Marshal(tokens)
if err != nil {
return nil, fmt.Errorf("Error marshalling sign-in request: %v", err)
}
req, err := hubRequest("POST", refreshResource, "", bytes.NewReader(reqBody))
if config.Trace {
log.Printf(">>>\n%s", identJson(reqBody))
}
if err != nil {
return nil, err
}
var jsResp SigninResponse
code, _, err := do(hubApi(), req, &jsResp)
if err != nil {
return nil, fmt.Errorf("Refresh API token error: %v", err)
}
if code != 200 {
return nil, fmt.Errorf("Got %d HTTP from Auth Service refresh API token, expected 200 HTTP", code)
}
if jsResp.AccessToken == "" {
return nil, fmt.Errorf("Empty or no `accessToken` in refresh API token response")
}
return &jsResp, nil
}