Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1160649

Browse files
author
Nándor István Krácser
authored
Merge pull request dexidp#1621 from concourse/pr/passowrd-grant-synced
Rework - add support for Resource Owner Password Credentials Grant
2 parents f17fa67 + 0f9a74f commit 1160649

6 files changed

Lines changed: 232 additions & 0 deletions

File tree

cmd/dex/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ type OAuth2 struct {
133133
SkipApprovalScreen bool `json:"skipApprovalScreen"`
134134
// If specified, show the connector selection screen even if there's only one
135135
AlwaysShowLoginScreen bool `json:"alwaysShowLoginScreen"`
136+
// This is the connector that can be used for password grant
137+
PasswordConnector string `json:"passwordConnector"`
136138
}
137139

138140
// Web is the config format for the HTTP server.

cmd/dex/serve.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ func serve(cmd *cobra.Command, args []string) error {
201201
if c.OAuth2.SkipApprovalScreen {
202202
logger.Infof("config skipping approval screen")
203203
}
204+
if c.OAuth2.PasswordConnector != "" {
205+
logger.Infof("config using password grant connector: %s", c.OAuth2.PasswordConnector)
206+
}
204207
if len(c.Web.AllowedOrigins) > 0 {
205208
logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins)
206209
}
@@ -212,6 +215,7 @@ func serve(cmd *cobra.Command, args []string) error {
212215
SupportedResponseTypes: c.OAuth2.ResponseTypes,
213216
SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
214217
AlwaysShowLoginScreen: c.OAuth2.AlwaysShowLoginScreen,
218+
PasswordConnector: c.OAuth2.PasswordConnector,
215219
AllowedOrigins: c.Web.AllowedOrigins,
216220
Issuer: c.Issuer,
217221
Storage: s,

examples/config-dev.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ telemetry:
5353
# go directly to it. For connected IdPs, this redirects the browser away
5454
# from application to upstream provider such as the Google login page
5555
# alwaysShowLoginScreen: false
56+
# Uncommend the passwordConnector to use a specific connector for password grants
57+
# passwordConnector: local
5658

5759
# Instead of reading from an external storage, use this list of clients.
5860
#

server/handlers.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,8 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
756756
s.handleAuthCode(w, r, client)
757757
case grantTypeRefreshToken:
758758
s.handleRefreshToken(w, r, client)
759+
case grantTypePassword:
760+
s.handlePasswordGrant(w, r, client)
759761
default:
760762
s.tokenErrHelper(w, errInvalidGrant, "", http.StatusBadRequest)
761763
}
@@ -1150,6 +1152,221 @@ func (s *Server) handleUserInfo(w http.ResponseWriter, r *http.Request) {
11501152
w.Write(claims)
11511153
}
11521154

1155+
func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, client storage.Client) {
1156+
// Parse the fields
1157+
if err := r.ParseForm(); err != nil {
1158+
s.tokenErrHelper(w, errInvalidRequest, "Couldn't parse data", http.StatusBadRequest)
1159+
return
1160+
}
1161+
q := r.Form
1162+
1163+
nonce := q.Get("nonce")
1164+
// Some clients, like the old go-oidc, provide extra whitespace. Tolerate this.
1165+
scopes := strings.Fields(q.Get("scope"))
1166+
1167+
// Parse the scopes if they are passed
1168+
var (
1169+
unrecognized []string
1170+
invalidScopes []string
1171+
)
1172+
hasOpenIDScope := false
1173+
for _, scope := range scopes {
1174+
switch scope {
1175+
case scopeOpenID:
1176+
hasOpenIDScope = true
1177+
case scopeOfflineAccess, scopeEmail, scopeProfile, scopeGroups, scopeFederatedID:
1178+
default:
1179+
peerID, ok := parseCrossClientScope(scope)
1180+
if !ok {
1181+
unrecognized = append(unrecognized, scope)
1182+
continue
1183+
}
1184+
1185+
isTrusted, err := s.validateCrossClientTrust(client.ID, peerID)
1186+
if err != nil {
1187+
s.tokenErrHelper(w, errInvalidClient, fmt.Sprintf("Error validating cross client trust %v.", err), http.StatusBadRequest)
1188+
return
1189+
}
1190+
if !isTrusted {
1191+
invalidScopes = append(invalidScopes, scope)
1192+
}
1193+
}
1194+
}
1195+
if !hasOpenIDScope {
1196+
s.tokenErrHelper(w, errInvalidRequest, `Missing required scope(s) ["openid"].`, http.StatusBadRequest)
1197+
return
1198+
}
1199+
if len(unrecognized) > 0 {
1200+
s.tokenErrHelper(w, errInvalidRequest, fmt.Sprintf("Unrecognized scope(s) %q", unrecognized), http.StatusBadRequest)
1201+
return
1202+
}
1203+
if len(invalidScopes) > 0 {
1204+
s.tokenErrHelper(w, errInvalidRequest, fmt.Sprintf("Client can't request scope(s) %q", invalidScopes), http.StatusBadRequest)
1205+
return
1206+
}
1207+
1208+
// Which connector
1209+
connID := s.passwordConnector
1210+
conn, err := s.getConnector(connID)
1211+
if err != nil {
1212+
s.tokenErrHelper(w, errInvalidRequest, "Requested connector does not exist.", http.StatusBadRequest)
1213+
return
1214+
}
1215+
1216+
passwordConnector, ok := conn.Connector.(connector.PasswordConnector)
1217+
if !ok {
1218+
s.tokenErrHelper(w, errInvalidRequest, "Requested password connector does not correct type.", http.StatusBadRequest)
1219+
return
1220+
}
1221+
1222+
// Login
1223+
username := q.Get("username")
1224+
password := q.Get("password")
1225+
identity, ok, err := passwordConnector.Login(r.Context(), parseScopes(scopes), username, password)
1226+
if err != nil {
1227+
s.tokenErrHelper(w, errInvalidRequest, "Could not login user", http.StatusBadRequest)
1228+
return
1229+
}
1230+
if !ok {
1231+
s.tokenErrHelper(w, errAccessDenied, "Invalid username or password", http.StatusUnauthorized)
1232+
return
1233+
}
1234+
1235+
// Build the claims to send the id token
1236+
claims := storage.Claims{
1237+
UserID: identity.UserID,
1238+
Username: identity.Username,
1239+
PreferredUsername: identity.PreferredUsername,
1240+
Email: identity.Email,
1241+
EmailVerified: identity.EmailVerified,
1242+
Groups: identity.Groups,
1243+
}
1244+
1245+
accessToken := storage.NewID()
1246+
idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, nonce, accessToken, connID)
1247+
if err != nil {
1248+
s.tokenErrHelper(w, errServerError, fmt.Sprintf("failed to create ID token: %v", err), http.StatusInternalServerError)
1249+
return
1250+
}
1251+
1252+
reqRefresh := func() bool {
1253+
// Ensure the connector supports refresh tokens.
1254+
//
1255+
// Connectors like `saml` do not implement RefreshConnector.
1256+
_, ok := conn.Connector.(connector.RefreshConnector)
1257+
if !ok {
1258+
return false
1259+
}
1260+
1261+
for _, scope := range scopes {
1262+
if scope == scopeOfflineAccess {
1263+
return true
1264+
}
1265+
}
1266+
return false
1267+
}()
1268+
var refreshToken string
1269+
if reqRefresh {
1270+
refresh := storage.RefreshToken{
1271+
ID: storage.NewID(),
1272+
Token: storage.NewID(),
1273+
ClientID: client.ID,
1274+
ConnectorID: connID,
1275+
Scopes: scopes,
1276+
Claims: claims,
1277+
Nonce: nonce,
1278+
// ConnectorData: authCode.ConnectorData,
1279+
CreatedAt: s.now(),
1280+
LastUsed: s.now(),
1281+
}
1282+
token := &internal.RefreshToken{
1283+
RefreshId: refresh.ID,
1284+
Token: refresh.Token,
1285+
}
1286+
if refreshToken, err = internal.Marshal(token); err != nil {
1287+
s.logger.Errorf("failed to marshal refresh token: %v", err)
1288+
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
1289+
return
1290+
}
1291+
1292+
if err := s.storage.CreateRefresh(refresh); err != nil {
1293+
s.logger.Errorf("failed to create refresh token: %v", err)
1294+
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
1295+
return
1296+
}
1297+
1298+
// deleteToken determines if we need to delete the newly created refresh token
1299+
// due to a failure in updating/creating the OfflineSession object for the
1300+
// corresponding user.
1301+
var deleteToken bool
1302+
defer func() {
1303+
if deleteToken {
1304+
// Delete newly created refresh token from storage.
1305+
if err := s.storage.DeleteRefresh(refresh.ID); err != nil {
1306+
s.logger.Errorf("failed to delete refresh token: %v", err)
1307+
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
1308+
return
1309+
}
1310+
}
1311+
}()
1312+
1313+
tokenRef := storage.RefreshTokenRef{
1314+
ID: refresh.ID,
1315+
ClientID: refresh.ClientID,
1316+
CreatedAt: refresh.CreatedAt,
1317+
LastUsed: refresh.LastUsed,
1318+
}
1319+
1320+
// Try to retrieve an existing OfflineSession object for the corresponding user.
1321+
if session, err := s.storage.GetOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID); err != nil {
1322+
if err != storage.ErrNotFound {
1323+
s.logger.Errorf("failed to get offline session: %v", err)
1324+
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
1325+
deleteToken = true
1326+
return
1327+
}
1328+
offlineSessions := storage.OfflineSessions{
1329+
UserID: refresh.Claims.UserID,
1330+
ConnID: refresh.ConnectorID,
1331+
Refresh: make(map[string]*storage.RefreshTokenRef),
1332+
}
1333+
offlineSessions.Refresh[tokenRef.ClientID] = &tokenRef
1334+
1335+
// Create a new OfflineSession object for the user and add a reference object for
1336+
// the newly received refreshtoken.
1337+
if err := s.storage.CreateOfflineSessions(offlineSessions); err != nil {
1338+
s.logger.Errorf("failed to create offline session: %v", err)
1339+
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
1340+
deleteToken = true
1341+
return
1342+
}
1343+
} else {
1344+
if oldTokenRef, ok := session.Refresh[tokenRef.ClientID]; ok {
1345+
// Delete old refresh token from storage.
1346+
if err := s.storage.DeleteRefresh(oldTokenRef.ID); err != nil {
1347+
s.logger.Errorf("failed to delete refresh token: %v", err)
1348+
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
1349+
deleteToken = true
1350+
return
1351+
}
1352+
}
1353+
1354+
// Update existing OfflineSession obj with new RefreshTokenRef.
1355+
if err := s.storage.UpdateOfflineSessions(session.UserID, session.ConnID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
1356+
old.Refresh[tokenRef.ClientID] = &tokenRef
1357+
return old, nil
1358+
}); err != nil {
1359+
s.logger.Errorf("failed to update offline session: %v", err)
1360+
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
1361+
deleteToken = true
1362+
return
1363+
}
1364+
}
1365+
}
1366+
1367+
s.writeAccessToken(w, idToken, accessToken, refreshToken, expiry)
1368+
}
1369+
11531370
func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, accessToken, refreshToken string, expiry time.Time) {
11541371
resp := struct {
11551372
AccessToken string `json:"access_token"`

server/oauth2.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ const (
121121
const (
122122
grantTypeAuthorizationCode = "authorization_code"
123123
grantTypeRefreshToken = "refresh_token"
124+
grantTypePassword = "password"
124125
)
125126

126127
const (

server/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ type Config struct {
7676
RotateKeysAfter time.Duration // Defaults to 6 hours.
7777
IDTokensValidFor time.Duration // Defaults to 24 hours
7878
AuthRequestsValidFor time.Duration // Defaults to 24 hours
79+
// If set, the server will use this connector to handle password grants
80+
PasswordConnector string
7981

8082
GCFrequency time.Duration // Defaults to 5 minutes
8183

@@ -145,6 +147,9 @@ type Server struct {
145147
// If enabled, show the connector selection screen even if there's only one
146148
alwaysShowLogin bool
147149

150+
// Used for password grant
151+
passwordConnector string
152+
148153
supportedResponseTypes map[string]bool
149154

150155
now func() time.Time
@@ -216,6 +221,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
216221
alwaysShowLogin: c.AlwaysShowLoginScreen,
217222
now: now,
218223
templates: tmpls,
224+
passwordConnector: c.PasswordConnector,
219225
logger: c.Logger,
220226
}
221227

0 commit comments

Comments
 (0)