forked from openshift/origin
-
Notifications
You must be signed in to change notification settings - Fork 1
/
basicauthpassword.go
145 lines (121 loc) · 4.75 KB
/
basicauthpassword.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package basicauthpassword
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/golang/glog"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
authapi "github.com/openshift/origin/pkg/oauthserver/api"
)
// Authenticator uses basic auth to make a request to a JSON-returning URL.
// A 401 status indicate failed auth.
// A non-200 status or the presence of an "error" key with a non-empty
// value indicates an error:
// {"error":"Error message"}
// A 200 status with an "id" key indicates success:
// {"id":"userid"}
// A successful response may also include name and/or email:
// {"id":"userid", "name": "User Name", "email":"user@example.com"}
type Authenticator struct {
providerName string
url string
client *http.Client
mapper authapi.UserIdentityMapper
}
// RemoteUserData holds user data returned from a remote basic-auth protected endpoint.
// These field names can not be changed unless external integrators are also updated.
// Names are based on standard OpenID Connect claims: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
type RemoteUserData struct {
// Subject - Identifier for the End-User at the Issuer. Required.
Subject string `json:"sub"`
// Name is the end-User's full name in displayable form including all name parts, possibly including titles and suffixes,
// ordered according to the End-User's locale and preferences. Optional.
Name string `json:"name"`
// PreferredUsername is a shorthand name by which the End-User wishes to be referred. Optional.
// Useful when the immutable subject is different than the login used by the user to authenticate
PreferredUsername string `json:"preferred_username"`
// Email is the end-User's preferred e-mail address. Optional.
Email string `json:"email"`
}
// RemoteError holds error data returned from a remote authentication request
type RemoteError struct {
Error string
}
var RedirectAttemptedError = errors.New("Redirect attempted")
// New returns an authenticator which will make a basic auth call to the given url.
// A custom transport can be provided (typically to customize TLS options like trusted roots or present a client certificate).
// If no transport is provided, http.DefaultTransport is used
func New(providerName string, url string, transport http.RoundTripper, mapper authapi.UserIdentityMapper) authenticator.Password {
if transport == nil {
transport = http.DefaultTransport
}
client := &http.Client{Transport: transport}
// We don't support redirects in the basic auth provider because it could be a malicious attempt to send credentials to
// another site.
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return RedirectAttemptedError
}
return &Authenticator{providerName, url, client, mapper}
}
func (a *Authenticator) AuthenticatePassword(username, password string) (user.Info, bool, error) {
req, err := http.NewRequest("GET", a.url, nil)
if err != nil {
return nil, false, err
}
// Basic auth does not support usernames containing colons
// http://tools.ietf.org/html/rfc2617#section-2
if strings.Contains(username, ":") {
return nil, false, fmt.Errorf("invalid username")
}
req.SetBasicAuth(username, password)
req.Header.Set("Accept", "application/json")
resp, err := a.client.Do(req)
if err != nil {
return nil, false, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
return nil, false, nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, false, err
}
remoteError := RemoteError{}
json.Unmarshal(body, &remoteError)
if remoteError.Error != "" {
return nil, false, errors.New(remoteError.Error)
}
if resp.StatusCode != http.StatusOK {
return nil, false, fmt.Errorf("An error occurred while authenticating (%d)", resp.StatusCode)
}
remoteUserData := RemoteUserData{}
err = json.Unmarshal(body, &remoteUserData)
if err != nil {
return nil, false, err
}
if len(remoteUserData.Subject) == 0 {
return nil, false, errors.New("Could not retrieve user data")
}
identity := authapi.NewDefaultUserIdentityInfo(a.providerName, remoteUserData.Subject)
if len(remoteUserData.Name) > 0 {
identity.Extra[authapi.IdentityDisplayNameKey] = remoteUserData.Name
}
if len(remoteUserData.PreferredUsername) > 0 {
identity.Extra[authapi.IdentityPreferredUsernameKey] = remoteUserData.PreferredUsername
}
if len(remoteUserData.Email) > 0 {
identity.Extra[authapi.IdentityEmailKey] = remoteUserData.Email
}
user, err := a.mapper.UserFor(identity)
if err != nil {
glog.V(4).Infof("Error creating or updating mapping for: %#v due to %v", identity, err)
return nil, false, err
}
glog.V(4).Infof("Got userIdentityMapping: %#v", user)
return user, true, nil
}