mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
311 lines
7.1 KiB
311 lines
7.1 KiB
// Package keystone provides authentication strategy using Keystone. |
|
package keystone |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"io/ioutil" |
|
"net/http" |
|
|
|
"github.com/dexidp/dex/connector" |
|
"github.com/dexidp/dex/pkg/log" |
|
) |
|
|
|
type conn struct { |
|
Domain string |
|
Host string |
|
AdminUsername string |
|
AdminPassword string |
|
Logger log.Logger |
|
} |
|
|
|
type userKeystone struct { |
|
Domain domainKeystone `json:"domain"` |
|
ID string `json:"id"` |
|
Name string `json:"name"` |
|
} |
|
|
|
type domainKeystone struct { |
|
ID string `json:"id"` |
|
Name string `json:"name"` |
|
} |
|
|
|
// Config holds the configuration parameters for Keystone connector. |
|
// Keystone should expose API v3 |
|
// An example config: |
|
// connectors: |
|
// type: keystone |
|
// id: keystone |
|
// name: Keystone |
|
// config: |
|
// keystoneHost: http://example:5000 |
|
// domain: default |
|
// keystoneUsername: demo |
|
// keystonePassword: DEMO_PASS |
|
type Config struct { |
|
Domain string `json:"domain"` |
|
Host string `json:"keystoneHost"` |
|
AdminUsername string `json:"keystoneUsername"` |
|
AdminPassword string `json:"keystonePassword"` |
|
} |
|
|
|
type loginRequestData struct { |
|
auth `json:"auth"` |
|
} |
|
|
|
type auth struct { |
|
Identity identity `json:"identity"` |
|
} |
|
|
|
type identity struct { |
|
Methods []string `json:"methods"` |
|
Password password `json:"password"` |
|
} |
|
|
|
type password struct { |
|
User user `json:"user"` |
|
} |
|
|
|
type user struct { |
|
Name string `json:"name"` |
|
Domain domain `json:"domain"` |
|
Password string `json:"password"` |
|
} |
|
|
|
type domain struct { |
|
ID string `json:"id"` |
|
} |
|
|
|
type token struct { |
|
User userKeystone `json:"user"` |
|
} |
|
|
|
type tokenResponse struct { |
|
Token token `json:"token"` |
|
} |
|
|
|
type group struct { |
|
ID string `json:"id"` |
|
Name string `json:"name"` |
|
} |
|
|
|
type groupsResponse struct { |
|
Groups []group `json:"groups"` |
|
} |
|
|
|
type userResponse struct { |
|
User struct { |
|
Name string `json:"name"` |
|
Email string `json:"email"` |
|
ID string `json:"id"` |
|
} `json:"user"` |
|
} |
|
|
|
var ( |
|
_ connector.PasswordConnector = &conn{} |
|
_ connector.RefreshConnector = &conn{} |
|
) |
|
|
|
// Open returns an authentication strategy using Keystone. |
|
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { |
|
return &conn{ |
|
c.Domain, |
|
c.Host, |
|
c.AdminUsername, |
|
c.AdminPassword, |
|
logger, |
|
}, nil |
|
} |
|
|
|
func (p *conn) Close() error { return nil } |
|
|
|
func (p *conn) Login(ctx context.Context, scopes connector.Scopes, username, password string) (identity connector.Identity, validPassword bool, err error) { |
|
resp, err := p.getTokenResponse(ctx, username, password) |
|
if err != nil { |
|
return identity, false, fmt.Errorf("keystone: error %v", err) |
|
} |
|
if resp.StatusCode/100 != 2 { |
|
return identity, false, fmt.Errorf("keystone login: error %v", resp.StatusCode) |
|
} |
|
if resp.StatusCode != 201 { |
|
return identity, false, nil |
|
} |
|
token := resp.Header.Get("X-Subject-Token") |
|
data, err := ioutil.ReadAll(resp.Body) |
|
if err != nil { |
|
return identity, false, err |
|
} |
|
defer resp.Body.Close() |
|
tokenResp := new(tokenResponse) |
|
err = json.Unmarshal(data, &tokenResp) |
|
if err != nil { |
|
return identity, false, fmt.Errorf("keystone: invalid token response: %v", err) |
|
} |
|
if scopes.Groups { |
|
groups, err := p.getUserGroups(ctx, tokenResp.Token.User.ID, token) |
|
if err != nil { |
|
return identity, false, err |
|
} |
|
identity.Groups = groups |
|
} |
|
identity.Username = username |
|
identity.UserID = tokenResp.Token.User.ID |
|
|
|
user, err := p.getUser(ctx, tokenResp.Token.User.ID, token) |
|
if err != nil { |
|
return identity, false, err |
|
} |
|
if user.User.Email != "" { |
|
identity.Email = user.User.Email |
|
identity.EmailVerified = true |
|
} |
|
|
|
return identity, true, nil |
|
} |
|
|
|
func (p *conn) Prompt() string { return "username" } |
|
|
|
func (p *conn) Refresh( |
|
ctx context.Context, scopes connector.Scopes, identity connector.Identity) (connector.Identity, error) { |
|
token, err := p.getAdminToken(ctx) |
|
if err != nil { |
|
return identity, fmt.Errorf("keystone: failed to obtain admin token: %v", err) |
|
} |
|
ok, err := p.checkIfUserExists(ctx, identity.UserID, token) |
|
if err != nil { |
|
return identity, err |
|
} |
|
if !ok { |
|
return identity, fmt.Errorf("keystone: user %q does not exist", identity.UserID) |
|
} |
|
if scopes.Groups { |
|
groups, err := p.getUserGroups(ctx, identity.UserID, token) |
|
if err != nil { |
|
return identity, err |
|
} |
|
identity.Groups = groups |
|
} |
|
return identity, nil |
|
} |
|
|
|
func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (response *http.Response, err error) { |
|
client := &http.Client{} |
|
jsonData := loginRequestData{ |
|
auth: auth{ |
|
Identity: identity{ |
|
Methods: []string{"password"}, |
|
Password: password{ |
|
User: user{ |
|
Name: username, |
|
Domain: domain{ID: p.Domain}, |
|
Password: pass, |
|
}, |
|
}, |
|
}, |
|
}, |
|
} |
|
jsonValue, err := json.Marshal(jsonData) |
|
if err != nil { |
|
return nil, err |
|
} |
|
// https://developer.openstack.org/api-ref/identity/v3/#password-authentication-with-unscoped-authorization |
|
authTokenURL := p.Host + "/v3/auth/tokens/" |
|
req, err := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(jsonValue)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
req = req.WithContext(ctx) |
|
|
|
return client.Do(req) |
|
} |
|
|
|
func (p *conn) getAdminToken(ctx context.Context) (string, error) { |
|
resp, err := p.getTokenResponse(ctx, p.AdminUsername, p.AdminPassword) |
|
if err != nil { |
|
return "", err |
|
} |
|
defer resp.Body.Close() |
|
|
|
token := resp.Header.Get("X-Subject-Token") |
|
return token, nil |
|
} |
|
|
|
func (p *conn) checkIfUserExists(ctx context.Context, userID string, token string) (bool, error) { |
|
user, err := p.getUser(ctx, userID, token) |
|
return user != nil, err |
|
} |
|
|
|
func (p *conn) getUser(ctx context.Context, userID string, token string) (*userResponse, error) { |
|
// https://developer.openstack.org/api-ref/identity/v3/#show-user-details |
|
userURL := p.Host + "/v3/users/" + userID |
|
client := &http.Client{} |
|
req, err := http.NewRequest("GET", userURL, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
req.Header.Set("X-Auth-Token", token) |
|
req = req.WithContext(ctx) |
|
resp, err := client.Do(req) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer resp.Body.Close() |
|
|
|
if resp.StatusCode != 200 { |
|
return nil, err |
|
} |
|
|
|
data, err := ioutil.ReadAll(resp.Body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
user := userResponse{} |
|
err = json.Unmarshal(data, &user) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return &user, nil |
|
} |
|
|
|
func (p *conn) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) { |
|
client := &http.Client{} |
|
// https://developer.openstack.org/api-ref/identity/v3/#list-groups-to-which-a-user-belongs |
|
groupsURL := p.Host + "/v3/users/" + userID + "/groups" |
|
req, err := http.NewRequest("GET", groupsURL, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
req.Header.Set("X-Auth-Token", token) |
|
req = req.WithContext(ctx) |
|
resp, err := client.Do(req) |
|
if err != nil { |
|
p.Logger.Errorf("keystone: error while fetching user %q groups\n", userID) |
|
return nil, err |
|
} |
|
|
|
data, err := ioutil.ReadAll(resp.Body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer resp.Body.Close() |
|
|
|
groupsResp := new(groupsResponse) |
|
|
|
err = json.Unmarshal(data, &groupsResp) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
groups := make([]string, len(groupsResp.Groups)) |
|
for i, group := range groupsResp.Groups { |
|
groups[i] = group.Name |
|
} |
|
return groups, nil |
|
}
|
|
|