|
|
|
@ -1,22 +1,15 @@ |
|
|
|
package api |
|
|
|
|
|
|
|
import ( |
|
|
|
"encoding/json" |
|
|
|
"regexp" |
|
|
|
|
|
|
|
"fmt" |
|
|
|
"net/http" |
|
|
|
"net/url" |
|
|
|
|
|
|
|
commonhttp "github.com/cloudtrust/common-service/errors" |
|
|
|
"github.com/cloudtrust/keycloak-client/v3" |
|
|
|
"github.com/nmasse-itix/keycloak-client" |
|
|
|
"github.com/pkg/errors" |
|
|
|
"gopkg.in/h2non/gentleman.v2" |
|
|
|
"gopkg.in/h2non/gentleman.v2/plugin" |
|
|
|
"gopkg.in/h2non/gentleman.v2/plugins/query" |
|
|
|
"gopkg.in/h2non/gentleman.v2/plugins/timeout" |
|
|
|
|
|
|
|
jwt "github.com/gbrlsnchs/jwt/v2" |
|
|
|
) |
|
|
|
|
|
|
|
// Client is the keycloak client.
|
|
|
|
@ -90,9 +83,6 @@ func (c *Client) GetToken(realm string, username string, password string) (strin |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// fmt.Printf("%s", accessToken.(string))
|
|
|
|
// fmt.Println()
|
|
|
|
|
|
|
|
return accessToken.(string), nil |
|
|
|
} |
|
|
|
|
|
|
|
@ -101,7 +91,7 @@ func (c *Client) get(accessToken string, data interface{}, plugins ...plugin.Plu |
|
|
|
var err error |
|
|
|
var req = c.httpClient.Get() |
|
|
|
req = applyPlugins(req, plugins...) |
|
|
|
req, err = setAuthorisationAndHostHeaders(req, accessToken) |
|
|
|
req = setAuthorisationHeader(req, accessToken) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
@ -115,23 +105,21 @@ func (c *Client) get(accessToken string, data interface{}, plugins ...plugin.Plu |
|
|
|
return errors.Wrap(err, keycloak.MsgErrCannotObtain+"."+keycloak.Response) |
|
|
|
} |
|
|
|
|
|
|
|
switch { |
|
|
|
case resp.StatusCode == http.StatusUnauthorized: |
|
|
|
return keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} |
|
|
|
case resp.StatusCode >= 400: |
|
|
|
return treatErrorStatus(resp) |
|
|
|
case resp.StatusCode >= 200: |
|
|
|
switch resp.Header.Get("Content-Type") { |
|
|
|
case "application/json": |
|
|
|
return resp.JSON(data) |
|
|
|
case "application/octet-stream": |
|
|
|
data = resp.Bytes() |
|
|
|
return nil |
|
|
|
default: |
|
|
|
return fmt.Errorf("%s.%v", keycloak.MsgErrUnkownHTTPContentType, resp.Header.Get("Content-Type")) |
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 { |
|
|
|
return keycloak.HTTPError{ |
|
|
|
HTTPStatus: resp.StatusCode, |
|
|
|
Message: string(resp.Bytes()), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
switch resp.Header.Get("Content-Type") { |
|
|
|
case "application/json": |
|
|
|
return resp.JSON(data) |
|
|
|
case "application/octet-stream": |
|
|
|
_ = resp.Bytes() |
|
|
|
return nil |
|
|
|
default: |
|
|
|
return fmt.Errorf("%s.%v", keycloak.MsgErrUnknownResponseStatusCode, resp.StatusCode) |
|
|
|
return fmt.Errorf("%s.%v", keycloak.MsgErrUnkownHTTPContentType, resp.Header.Get("Content-Type")) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -140,7 +128,7 @@ func (c *Client) post(accessToken string, data interface{}, plugins ...plugin.Pl |
|
|
|
var err error |
|
|
|
var req = c.httpClient.Post() |
|
|
|
req = applyPlugins(req, plugins...) |
|
|
|
req, err = setAuthorisationAndHostHeaders(req, accessToken) |
|
|
|
req = setAuthorisationHeader(req, accessToken) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return "", err |
|
|
|
@ -154,25 +142,23 @@ func (c *Client) post(accessToken string, data interface{}, plugins ...plugin.Pl |
|
|
|
return "", errors.Wrap(err, keycloak.MsgErrCannotObtain+"."+keycloak.Response) |
|
|
|
} |
|
|
|
|
|
|
|
switch { |
|
|
|
case resp.StatusCode == http.StatusUnauthorized: |
|
|
|
return "", keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} |
|
|
|
case resp.StatusCode >= 400: |
|
|
|
return "", treatErrorStatus(resp) |
|
|
|
case resp.StatusCode >= 200: |
|
|
|
var location = resp.Header.Get("Location") |
|
|
|
|
|
|
|
switch resp.Header.Get("Content-Type") { |
|
|
|
case "application/json": |
|
|
|
return location, resp.JSON(data) |
|
|
|
case "application/octet-stream": |
|
|
|
data = resp.Bytes() |
|
|
|
return location, nil |
|
|
|
default: |
|
|
|
return location, nil |
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 { |
|
|
|
return "", keycloak.HTTPError{ |
|
|
|
HTTPStatus: resp.StatusCode, |
|
|
|
Message: string(resp.Bytes()), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var location = resp.Header.Get("Location") |
|
|
|
|
|
|
|
switch resp.Header.Get("Content-Type") { |
|
|
|
case "application/json": |
|
|
|
return location, resp.JSON(data) |
|
|
|
case "application/octet-stream": |
|
|
|
data = resp.Bytes() |
|
|
|
return location, nil |
|
|
|
default: |
|
|
|
return "", fmt.Errorf("%s.%v", keycloak.MsgErrUnknownResponseStatusCode, resp.StatusCode) |
|
|
|
return location, nil |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -181,7 +167,7 @@ func (c *Client) delete(accessToken string, plugins ...plugin.Plugin) error { |
|
|
|
var err error |
|
|
|
var req = c.httpClient.Delete() |
|
|
|
req = applyPlugins(req, plugins...) |
|
|
|
req, err = setAuthorisationAndHostHeaders(req, accessToken) |
|
|
|
req = setAuthorisationHeader(req, accessToken) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
@ -195,19 +181,14 @@ func (c *Client) delete(accessToken string, plugins ...plugin.Plugin) error { |
|
|
|
return errors.Wrap(err, keycloak.MsgErrCannotObtain+"."+keycloak.Response) |
|
|
|
} |
|
|
|
|
|
|
|
switch { |
|
|
|
case resp.StatusCode == http.StatusUnauthorized: |
|
|
|
return keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} |
|
|
|
case resp.StatusCode >= 400: |
|
|
|
return treatErrorStatus(resp) |
|
|
|
case resp.StatusCode >= 200: |
|
|
|
return nil |
|
|
|
default: |
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 { |
|
|
|
return keycloak.HTTPError{ |
|
|
|
HTTPStatus: resp.StatusCode, |
|
|
|
Message: string(resp.Bytes()), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -215,7 +196,7 @@ func (c *Client) put(accessToken string, plugins ...plugin.Plugin) error { |
|
|
|
var err error |
|
|
|
var req = c.httpClient.Put() |
|
|
|
req = applyPlugins(req, plugins...) |
|
|
|
req, err = setAuthorisationAndHostHeaders(req, accessToken) |
|
|
|
req = setAuthorisationHeader(req, accessToken) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
@ -229,35 +210,21 @@ func (c *Client) put(accessToken string, plugins ...plugin.Plugin) error { |
|
|
|
return errors.Wrap(err, keycloak.MsgErrCannotObtain+"."+keycloak.Response) |
|
|
|
} |
|
|
|
|
|
|
|
switch { |
|
|
|
case resp.StatusCode == http.StatusUnauthorized: |
|
|
|
return keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} |
|
|
|
case resp.StatusCode >= 400: |
|
|
|
return treatErrorStatus(resp) |
|
|
|
case resp.StatusCode >= 200: |
|
|
|
return nil |
|
|
|
default: |
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 { |
|
|
|
return keycloak.HTTPError{ |
|
|
|
HTTPStatus: resp.StatusCode, |
|
|
|
Message: string(resp.Bytes()), |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func setAuthorisationAndHostHeaders(req *gentleman.Request, accessToken string) (*gentleman.Request, error) { |
|
|
|
host, err := extractHostFromToken(accessToken) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return req, err |
|
|
|
return nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func setAuthorisationHeader(req *gentleman.Request, accessToken string) *gentleman.Request { |
|
|
|
var r = req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", accessToken)) |
|
|
|
r = r.SetHeader("X-Forwarded-Proto", "https") |
|
|
|
|
|
|
|
r.Context.Request.Host = host |
|
|
|
|
|
|
|
return r, nil |
|
|
|
return r |
|
|
|
} |
|
|
|
|
|
|
|
// applyPlugins apply all the plugins to the request req.
|
|
|
|
@ -269,41 +236,6 @@ func applyPlugins(req *gentleman.Request, plugins ...plugin.Plugin) *gentleman.R |
|
|
|
return r |
|
|
|
} |
|
|
|
|
|
|
|
func extractHostFromToken(token string) (string, error) { |
|
|
|
issuer, err := extractIssuerFromToken(token) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return "", err |
|
|
|
} |
|
|
|
|
|
|
|
var u *url.URL |
|
|
|
{ |
|
|
|
var err error |
|
|
|
u, err = url.Parse(issuer) |
|
|
|
if err != nil { |
|
|
|
return "", errors.Wrap(err, keycloak.MsgErrCannotParse+"."+keycloak.TokenProviderURL) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return u.Host, nil |
|
|
|
} |
|
|
|
|
|
|
|
func extractIssuerFromToken(token string) (string, error) { |
|
|
|
payload, _, err := jwt.Parse(token) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return "", errors.Wrap(err, keycloak.MsgErrCannotParse+"."+keycloak.TokenMsg) |
|
|
|
} |
|
|
|
|
|
|
|
var jot Token |
|
|
|
|
|
|
|
if err = jwt.Unmarshal(payload, &jot); err != nil { |
|
|
|
return "", errors.Wrap(err, keycloak.MsgErrCannotUnmarshal+"."+keycloak.TokenMsg) |
|
|
|
} |
|
|
|
|
|
|
|
return jot.Issuer, nil |
|
|
|
} |
|
|
|
|
|
|
|
// createQueryPlugins create query parameters with the key values paramKV.
|
|
|
|
func createQueryPlugins(paramKV ...string) []plugin.Plugin { |
|
|
|
var plugins = []plugin.Plugin{} |
|
|
|
@ -314,70 +246,3 @@ func createQueryPlugins(paramKV ...string) []plugin.Plugin { |
|
|
|
} |
|
|
|
return plugins |
|
|
|
} |
|
|
|
|
|
|
|
func treatErrorStatus(resp *gentleman.Response) error { |
|
|
|
var response map[string]interface{} |
|
|
|
err := json.Unmarshal(resp.Bytes(), &response) |
|
|
|
if message, ok := response["errorMessage"]; ok && err == nil { |
|
|
|
return whitelistErrors(resp.StatusCode, message.(string)) |
|
|
|
} |
|
|
|
return keycloak.HTTPError{ |
|
|
|
HTTPStatus: resp.StatusCode, |
|
|
|
Message: string(resp.Bytes()), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func whitelistErrors(statusCode int, message string) error { |
|
|
|
// whitelist errors from Keycloak
|
|
|
|
reg := regexp.MustCompile("invalidPassword[a-zA-Z]*Message") |
|
|
|
errorMessages := map[string]string{ |
|
|
|
"User exists with same username or email": keycloak.MsgErrExistingValue + "." + keycloak.UserOrEmail, |
|
|
|
"usernameExistsMessage": keycloak.MsgErrExistingValue + "." + keycloak.UserOrEmail, |
|
|
|
"emailExistsMessage": keycloak.MsgErrExistingValue + "." + keycloak.UserOrEmail, |
|
|
|
"User exists with same username": keycloak.MsgErrExistingValue + "." + keycloak.Username, |
|
|
|
"User exists with same email": keycloak.MsgErrExistingValue + "." + keycloak.Email, |
|
|
|
"readOnlyUsernameMessage": keycloak.MsgErrReadOnly + "." + keycloak.Username, |
|
|
|
} |
|
|
|
|
|
|
|
switch { |
|
|
|
//POST account/credentials/password with error message related to invalid value for the password
|
|
|
|
// of the format invalidPassword{a-zA-Z}*Message, e.g. invalidPasswordMinDigitsMessage
|
|
|
|
case reg.MatchString(message): |
|
|
|
return commonhttp.Error{ |
|
|
|
Status: statusCode, |
|
|
|
Message: "keycloak." + message, |
|
|
|
} |
|
|
|
// update account in back-office or self-service
|
|
|
|
case errorMessages[message] != "": |
|
|
|
return commonhttp.Error{ |
|
|
|
Status: statusCode, |
|
|
|
Message: "keycloak." + errorMessages[message], |
|
|
|
} |
|
|
|
default: |
|
|
|
return keycloak.HTTPError{ |
|
|
|
HTTPStatus: statusCode, |
|
|
|
Message: message, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Token is JWT token.
|
|
|
|
// We need to define our own structure as the library define aud as a string but it can also be a string array.
|
|
|
|
// To fix this issue, we remove aud as we do not use it here.
|
|
|
|
type Token struct { |
|
|
|
hdr *header |
|
|
|
Issuer string `json:"iss,omitempty"` |
|
|
|
Subject string `json:"sub,omitempty"` |
|
|
|
ExpirationTime int64 `json:"exp,omitempty"` |
|
|
|
NotBefore int64 `json:"nbf,omitempty"` |
|
|
|
IssuedAt int64 `json:"iat,omitempty"` |
|
|
|
ID string `json:"jti,omitempty"` |
|
|
|
Username string `json:"preferred_username,omitempty"` |
|
|
|
} |
|
|
|
|
|
|
|
type header struct { |
|
|
|
Algorithm string `json:"alg,omitempty"` |
|
|
|
KeyID string `json:"kid,omitempty"` |
|
|
|
Type string `json:"typ,omitempty"` |
|
|
|
ContentType string `json:"cty,omitempty"` |
|
|
|
} |
|
|
|
|