Skip to content

Commit 0963f8a

Browse files
committed
Display errors during IDP authentication
- login callback page will output the error returned by the IDP in case of a misconfiguration - User can configured desired IDP scopes via CONSOLE_IDP_SCOPES env variable - User can configured desired IDP Token expiration time via CONSOLE_IDP_TOKEN_EXPIRATION env variable
1 parent 94747ac commit 0963f8a

File tree

4 files changed

+72
-27
lines changed

4 files changed

+72
-27
lines changed

pkg/auth/idp/oauth2/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,13 @@ var defaultSaltForIdpHmac = utils.RandomCharString(64)
6666
func getSaltForIdpHmac() string {
6767
return env.Get(ConsoleIdpHmacSalt, defaultSaltForIdpHmac)
6868
}
69+
70+
// getIdpScopes return default scopes during the IDP login request
71+
func getIdpScopes() string {
72+
return env.Get(ConsoleIDPScopes, "openid,profile,app_metadata,user_metadata,email")
73+
}
74+
75+
// getIdpTokenExpiration return default token expiration for access token (in seconds)
76+
func getIdpTokenExpiration() string {
77+
return env.Get(ConsoleIDPTokenExpiration, "3600")
78+
}

pkg/auth/idp/oauth2/const.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package oauth2
1818

1919
const (
2020
// const for idp configuration
21-
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
22-
ConsoleIdpURL = "CONSOLE_IDP_URL"
23-
ConsoleIdpClientID = "CONSOLE_IDP_CLIENT_ID"
24-
ConsoleIdpSecret = "CONSOLE_IDP_SECRET"
25-
ConsoleIdpCallbackURL = "CONSOLE_IDP_CALLBACK"
26-
ConsoleIdpHmacPassphrase = "CONSOLE_IDP_HMAC_PASSPHRASE"
27-
ConsoleIdpHmacSalt = "CONSOLE_IDP_HMAC_SALT"
21+
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
22+
ConsoleIdpURL = "CONSOLE_IDP_URL"
23+
ConsoleIdpClientID = "CONSOLE_IDP_CLIENT_ID"
24+
ConsoleIdpSecret = "CONSOLE_IDP_SECRET"
25+
ConsoleIdpCallbackURL = "CONSOLE_IDP_CALLBACK"
26+
ConsoleIdpHmacPassphrase = "CONSOLE_IDP_HMAC_PASSPHRASE"
27+
ConsoleIdpHmacSalt = "CONSOLE_IDP_HMAC_SALT"
28+
ConsoleIDPScopes = "CONSOLE_IDP_SCOPES"
29+
ConsoleIDPTokenExpiration = "CONSOLE_IDP_TOKEN_EXPIRATION"
2830
)

pkg/auth/idp/oauth2/provider.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"log"
2626
"net/http"
2727
"net/url"
28+
"strconv"
2829
"strings"
2930
"time"
3031

@@ -115,12 +116,14 @@ func NewOauth2ProviderClient(ctx context.Context, scopes []string, httpClient *h
115116
if err != nil {
116117
return nil, err
117118
}
119+
// below verification should not be necessary if the user configure exactly the
120+
// scopes he need, will be removed on a future release
118121
if u.Host == "google.com" {
119122
scopes = []string{oidc.ScopeOpenID}
120123
}
121-
// If provided scopes are empty we use a default list
124+
// If provided scopes are empty we use a default list or the user configured list
122125
if len(scopes) == 0 {
123-
scopes = []string{oidc.ScopeOpenID, "profile", "app_metadata", "user_metadata", "email"}
126+
scopes = strings.Split(getIdpScopes(), ",")
124127
}
125128
client := new(Provider)
126129
client.oauth2Config = &xoauth2.Config{
@@ -177,9 +180,22 @@ func (client *Provider) VerifyIdentity(ctx context.Context, code, state string)
177180
return nil, errors.New("invalid token")
178181
}
179182

183+
// expiration configured in the token itself
184+
expiration := int(oauth2Token.Expiry.Sub(time.Now().UTC()).Seconds())
185+
186+
// check if user configured a hardcoded expiration for console via env variables
187+
// and override the incoming expiration
188+
userConfiguredExpiration := getIdpTokenExpiration()
189+
if userConfiguredExpiration != "" {
190+
expiration, _ = strconv.Atoi(userConfiguredExpiration)
191+
}
192+
idToken := oauth2Token.Extra("id_token")
193+
if idToken == nil {
194+
return nil, errors.New("returned token is missing id_token claim")
195+
}
180196
return &credentials.WebIdentityToken{
181-
Token: oauth2Token.Extra("id_token").(string),
182-
Expiry: int(oauth2Token.Expiry.Sub(time.Now().UTC()).Seconds()),
197+
Token: idToken.(string),
198+
Expiry: expiration,
183199
}, nil
184200
}
185201
stsEndpoint := GetSTSEndpoint()

portal-ui/src/screens/LoginPage/LoginCallback.tsx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,48 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
import React, { FC, useEffect } from "react"; // eslint-disable-line @typescript-eslint/no-unused-vars
17+
import React, { FC, useEffect, useState } from "react"; // eslint-disable-line @typescript-eslint/no-unused-vars
1818
import { RouteComponentProps } from "react-router";
1919
import storage from "local-storage-fallback";
2020
import api from "../../common/api";
2121

2222
const LoginCallback: FC<RouteComponentProps> = ({ location }) => {
23+
const [error, setError] = useState<string>("");
24+
const [errorDescription, setErrorDescription] = useState<string>("");
2325
useEffect(() => {
2426
const code = (location.search.match(/code=([^&]+)/) || [])[1];
2527
const state = (location.search.match(/state=([^&]+)/) || [])[1];
26-
api
27-
.invoke("POST", "/api/v1/login/oauth2/auth", { code, state })
28-
.then((res: any) => {
29-
if (res && res.sessionId) {
30-
// store the jwt token
31-
storage.setItem("token", res.sessionId);
32-
// We push to history the new URL.
33-
window.location.href = "/";
34-
}
35-
})
36-
.catch((res: any) => {
37-
window.location.href = "/login";
38-
});
39-
// eslint-disable-next-line react-hooks/exhaustive-deps
28+
const error = (location.search.match(/error=([^&]+)/) || [])[1];
29+
const errorDescription = (location.search.match(
30+
/error_description=([^&]+)/
31+
) || [])[1];
32+
if (error != undefined || errorDescription != undefined) {
33+
setError(error);
34+
setErrorDescription(errorDescription);
35+
} else {
36+
api
37+
.invoke("POST", "/api/v1/login/oauth2/auth", { code, state })
38+
.then((res: any) => {
39+
if (res && res.sessionId) {
40+
// store the jwt token
41+
storage.setItem("token", res.sessionId);
42+
// We push to history the new URL.
43+
window.location.href = "/";
44+
}
45+
})
46+
.catch((res: any) => {
47+
window.location.href = "/login";
48+
});
49+
// eslint-disable-next-line react-hooks/exhaustive-deps
50+
}
4051
}, []);
41-
return null;
52+
return error != "" || errorDescription != "" ? (
53+
<div>
54+
<h2>IDP Error:</h2>
55+
<p>{error}</p>
56+
<p>{errorDescription}</p>
57+
</div>
58+
) : null;
4259
};
4360

4461
export default LoginCallback;

0 commit comments

Comments
 (0)