draft OIDC support

This commit is contained in:
klizhentas 2016-04-02 22:20:51 -07:00
parent b0bdd3e248
commit 84cade14c5
20 changed files with 968 additions and 134 deletions

View file

@ -20,6 +20,7 @@ import (
"crypto/x509"
"fmt"
"net"
"net/url"
"os"
"syscall"
@ -488,3 +489,39 @@ func IsTrustError(e error) bool {
_, ok := e.(te)
return ok
}
// OAuth2Error is error returned during OAuth2 authentication
// currently used in OIDC (OpenID Connect flow)
type OAuth2Error struct {
Code string `code:"code"`
Message string `message:"message"`
Query url.Values `query:"query"`
}
// NewOAuth2Error returns new instance of OAuth2Error
func NewOAuth2Error(code, message string, query url.Values) *OAuth2Error {
return &OAuth2Error{
Code: code,
Message: message,
Query: query,
}
}
// Error returns debug friendly error message
func (o *OAuth2Error) Error() string {
return fmt.Sprintf("OAuth2 error code=%v, message=%v", o.Code, o.Message)
}
// IsOAuth2Error indicates that this error of OAuth2 type
func (o *OAuth2Error) IsOAuth2Error() bool {
return true
}
// IsOAuth2Error returns if this is a OAuth2-related error
func IsOAuth2Error(e error) bool {
type oe interface {
IsOAuth2Error() bool
}
_, ok := e.(oe)
return ok
}

View file

@ -19,6 +19,7 @@ package auth
import (
"fmt"
"net/http"
"net/url"
"strconv"
"time"
@ -127,6 +128,14 @@ func NewAPIServer(a *AuthWithRoles) *APIServer {
srv.GET("/v1/sessions", httplib.MakeHandler(srv.getSessions))
srv.GET("/v1/sessions/:id", httplib.MakeHandler(srv.getSession))
// OIDC stuff
srv.POST("/v1/oidc/connectors", httplib.MakeHandler(srv.upsertOIDCConnector))
srv.GET("/v1/oidc/connectors", httplib.MakeHandler(srv.getOIDCConnectors))
srv.GET("/v1/oidc/connectors/:id", httplib.MakeHandler(srv.getOIDCConnector))
srv.DELETE("/v1/oidc/connectors/:id", httplib.MakeHandler(srv.deleteOIDCConnector))
srv.POST("/v1/oidc/requests/create", httplib.MakeHandler(srv.createOIDCAuthRequest))
srv.POST("/v1/oidc/requests/validate", httplib.MakeHandler(srv.validateOIDCAuthCallback))
return srv
}
@ -759,8 +768,7 @@ func (s *APIServer) getSignupTokenData(w http.ResponseWriter, r *http.Request, p
}
type createSignupTokenReq struct {
User string `json:"user"`
AllowedLogins []string `json:"allowed_logins"`
User services.User `json:"user"`
}
func (s *APIServer) createSignupToken(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
@ -768,7 +776,7 @@ func (s *APIServer) createSignupToken(w http.ResponseWriter, r *http.Request, p
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
token, err := s.a.CreateSignupToken(req.User, req.AllowedLogins)
token, err := s.a.CreateSignupToken(req.User)
if err != nil {
return nil, trace.Wrap(err)
}
@ -793,6 +801,101 @@ func (s *APIServer) createUserWithToken(w http.ResponseWriter, r *http.Request,
return sess, nil
}
type upsertOIDCConnectorReq struct {
Connector services.OIDCConnector `json:"connector"`
TTL time.Duration `json:"ttl"`
}
func (s *APIServer) upsertOIDCConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *upsertOIDCConnectorReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
err := s.a.UpsertOIDCConnector(req.Connector, req.TTL)
if err != nil {
return nil, trace.Wrap(err)
}
return message("ok"), nil
}
func parseBool(q url.Values, name string) (bool, error) {
stringVal := q.Get(name)
if stringVal == "" {
return false, nil
}
val, err := strconv.ParseBool(stringVal)
if err != nil {
return false, trace.Wrap(
teleport.BadParameter(
name, fmt.Sprintf("expected 'true' or 'false', got %v", stringVal)))
}
return val, nil
}
func (s *APIServer) getOIDCConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
withSecrets, err := parseBool(r.URL.Query(), "with_secrets")
if err != nil {
return nil, trace.Wrap(err)
}
connector, err := s.a.GetOIDCConnector(p[0].Value, withSecrets)
if err != nil {
return nil, trace.Wrap(err)
}
return connector, nil
}
func (s *APIServer) deleteOIDCConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
err := s.a.DeleteOIDCConnector(p[0].Value)
if err != nil {
return nil, trace.Wrap(err)
}
return message("ok"), nil
}
func (s *APIServer) getOIDCConnectors(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
withSecrets, err := parseBool(r.URL.Query(), "with_secrets")
if err != nil {
return nil, trace.Wrap(err)
}
connectors, err := s.a.GetOIDCConnectors(withSecrets)
if err != nil {
return nil, trace.Wrap(err)
}
return connectors, nil
}
type createOIDCAuthRequestReq struct {
Req services.OIDCAuthRequest `json:"req"`
}
func (s *APIServer) createOIDCAuthRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createOIDCAuthRequestReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
response, err := s.a.CreateOIDCAuthRequest(req.Req)
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
type validateOIDCAuthCallbackReq struct {
Query url.Values `json:"query"`
}
func (s *APIServer) validateOIDCAuthCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *validateOIDCAuthCallbackReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
response, err := s.a.ValidateOIDCAuthCallback(req.Query)
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
func message(msg string) map[string]interface{} {
return map[string]interface{}{"message": msg}
}

View file

@ -55,7 +55,7 @@ type APISuite struct {
LockS *services.LockService
PresenceS *services.PresenceService
ProvisioningS *services.ProvisioningService
WebS *services.WebService
WebS *services.IdentityService
}
var _ = Suite(&APISuite{})
@ -101,7 +101,7 @@ func (s *APISuite) SetUpTest(c *C) {
s.LockS = services.NewLockService(s.bk)
s.PresenceS = services.NewPresenceService(s.bk)
s.ProvisioningS = services.NewProvisioningService(s.bk)
s.WebS = services.NewWebService(s.bk)
s.WebS = services.NewIdentityService(s.bk)
}
func (s *APISuite) TearDownTest(c *C) {

View file

@ -25,16 +25,20 @@ package auth
import (
"fmt"
"net/url"
"os"
"sync"
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
log "github.com/Sirupsen/logrus"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
)
@ -86,10 +90,11 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) *AuthServer {
LockService: services.NewLockService(cfg.Backend),
PresenceService: services.NewPresenceService(cfg.Backend),
ProvisioningService: services.NewProvisioningService(cfg.Backend),
WebService: services.NewWebService(cfg.Backend),
IdentityService: services.NewIdentityService(cfg.Backend),
BkKeysService: services.NewBkKeysService(cfg.Backend),
DomainName: cfg.DomainName,
AuthServiceName: cfg.AuthServiceName,
oidcClients: make(map[string]*oidc.Client),
}
for _, o := range opts {
o(&as)
@ -109,8 +114,10 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) *AuthServer {
// - same for users and their sessions
// - checks public keys to see if they're signed by it (can be trusted or not)
type AuthServer struct {
clock clockwork.Clock
bk backend.Backend
sync.Mutex
oidcClients map[string]*oidc.Client
clock clockwork.Clock
bk backend.Backend
Authority
// DomainName stores the FQDN of the signing CA (its certificate will have this
@ -126,7 +133,7 @@ type AuthServer struct {
*services.LockService
*services.PresenceService
*services.ProvisioningService
*services.WebService
*services.IdentityService
*services.BkKeysService
}
@ -370,11 +377,11 @@ func (s *AuthServer) NewWebSession(userName string) (*Session, error) {
}
func (s *AuthServer) UpsertWebSession(user string, sess *Session, ttl time.Duration) error {
return s.WebService.UpsertWebSession(user, sess.ID, sess.WS, ttl)
return s.IdentityService.UpsertWebSession(user, sess.ID, sess.WS, ttl)
}
func (s *AuthServer) GetWebSession(userName string, id string) (*Session, error) {
ws, err := s.WebService.GetWebSession(userName, id)
ws, err := s.IdentityService.GetWebSession(userName, id)
if err != nil {
return nil, trace.Wrap(err)
}
@ -390,7 +397,7 @@ func (s *AuthServer) GetWebSession(userName string, id string) (*Session, error)
}
func (s *AuthServer) GetWebSessionInfo(userName string, id string) (*Session, error) {
sess, err := s.WebService.GetWebSession(userName, id)
sess, err := s.IdentityService.GetWebSession(userName, id)
if err != nil {
return nil, trace.Wrap(err)
}
@ -407,7 +414,170 @@ func (s *AuthServer) GetWebSessionInfo(userName string, id string) (*Session, er
}
func (s *AuthServer) DeleteWebSession(user string, id string) error {
return trace.Wrap(s.WebService.DeleteWebSession(user, id))
return trace.Wrap(s.IdentityService.DeleteWebSession(user, id))
}
func (s *AuthServer) getOIDCClient(conn *services.OIDCConnector) (*oidc.Client, error) {
s.Lock()
defer s.Unlock()
client, ok := s.oidcClients[conn.ID]
if ok {
return client, nil
}
config := oidc.ClientConfig{
RedirectURL: conn.RedirectURL,
Credentials: oidc.ClientCredentials{
ID: conn.ClientID,
Secret: conn.ClientSecret,
},
}
client, err := oidc.NewClient(config)
if err != nil {
return nil, trace.Wrap(err)
}
client.SyncProviderConfig(conn.IssuerURL)
s.oidcClients[conn.ID] = client
return client, nil
}
func (s *AuthServer) CreateOIDCAuthRequest(req services.OIDCAuthRequest) (*services.OIDCAuthRequest, error) {
connector, err := s.IdentityService.GetOIDCConnector(req.ConnectorID, true)
if err != nil {
return nil, trace.Wrap(err)
}
oidcClient, err := s.getOIDCClient(connector)
if err != nil {
return nil, trace.Wrap(err)
}
token, err := utils.CryptoRandomHex(WebSessionTokenLenBytes)
if err != nil {
return nil, trace.Wrap(err)
}
req.StateToken = token
oauthClient, err := oidcClient.OAuthClient()
if err != nil {
return nil, trace.Wrap(err)
}
sessionKey, err := utils.CryptoRandomHex(64)
if err != nil {
return nil, trace.Wrap(err)
}
redirectURL := oauthClient.AuthCodeURL(sessionKey, "", "")
req.RedirectURL = redirectURL
err = s.IdentityService.CreateOIDCAuthRequest(req, defaults.OIDCAuthRequestTTL)
if err != nil {
return nil, trace.Wrap(err)
}
return &req, nil
}
// OIDCAuthResponse is returned when auth server validated callback parameters
// returned from OIDC provider
type OIDCAuthResponse struct {
// User is authenticated teleport user
User services.User `json:"user"`
// Web session will be generated by auth server if requested in OIDCAuthRequest
Session *Session `json:"session,omitempty"`
// Cert will be generated by certificate authority
Cert []byte `json:"cert,omitempty"`
// Req is original oidc auth request
Req services.OIDCAuthRequest `json:"req"`
}
// ValidateOIDCAuthCallback is called by the proxy to check OIDC query parameters
// returned by OIDC Provider, if everything checks out, auth server
// will respond with OIDCAuthResponse, otherwise it will return error
func (a *AuthServer) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, error) {
if error := q.Get("error"); error != "" {
return nil, trace.Wrap(teleport.NewOAuth2Error(
oauth2.ErrorInvalidRequest, error, q))
}
code := q.Get("code")
if code == "" {
return nil, trace.Wrap(teleport.NewOAuth2Error(
oauth2.ErrorInvalidRequest, "code query param must be set", q))
}
stateToken := q.Get("state")
if stateToken == "" {
return nil, trace.Wrap(teleport.NewOAuth2Error(
oauth2.ErrorInvalidRequest, "missing state query param", q))
}
req, err := a.IdentityService.GetOIDCAuthRequest(stateToken)
if err != nil {
return nil, trace.Wrap(err)
}
connector, err := a.IdentityService.GetOIDCConnector(req.ConnectorID, true)
if err != nil {
return nil, trace.Wrap(err)
}
oidcClient, err := a.getOIDCClient(connector)
if err != nil {
return nil, trace.Wrap(err)
}
tok, err := oidcClient.ExchangeAuthCode(code)
if err != nil {
return nil, trace.Wrap(teleport.NewOAuth2Error(
oauth2.ErrorUnsupportedResponseType,
"unable to verify auth code with issuer", q))
}
claims, err := tok.Claims()
if err != nil {
return nil, trace.Wrap(teleport.NewOAuth2Error(
oauth2.ErrorUnsupportedResponseType, "unable to construct claims", q))
}
ident, err := oidc.IdentityFromClaims(claims)
if err != nil {
return nil, trace.Wrap(teleport.NewOAuth2Error(
oauth2.ErrorUnsupportedResponseType, "unable to convert claims to identity", q))
}
user, err := a.IdentityService.GetUserByOIDCIdentity(services.OIDCIdentity{
ConnectorID: req.ConnectorID, Email: ident.Email})
if err != nil {
return nil, trace.Wrap(err)
}
response := &OIDCAuthResponse{
User: *user,
}
if req.CreateWebSession {
sess, err := a.NewWebSession(user.Name)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.UpsertWebSession(user.Name, sess, WebSessionTTL); err != nil {
return nil, trace.Wrap(err)
}
response.Session = sess
}
if len(req.PublicKey) != 0 {
cert, err := a.GenerateUserCert(req.PublicKey, user.Name, req.CertTTL)
if err != nil {
return nil, trace.Wrap(err)
}
response.Cert = cert
}
return response, nil
}
const (

View file

@ -17,6 +17,7 @@ limitations under the License.
package auth
import (
"net/url"
"time"
"github.com/gravitational/teleport"
@ -343,11 +344,11 @@ func (a *AuthWithRoles) GenerateUserCert(key []byte, user string, ttl time.Durat
return a.authServer.GenerateUserCert(key, user, ttl)
}
}
func (a *AuthWithRoles) CreateSignupToken(user string, mappings []string) (token string, e error) {
func (a *AuthWithRoles) CreateSignupToken(user services.User) (token string, e error) {
if err := a.permChecker.HasPermission(a.role, ActionCreateSignupToken); err != nil {
return "", trace.Wrap(err)
} else {
return a.authServer.CreateSignupToken(user, mappings)
return a.authServer.CreateSignupToken(user)
}
}
@ -375,3 +376,57 @@ func (a *AuthWithRoles) UpsertUser(u services.User) error {
return a.authServer.UpsertUser(u)
}
}
func (a *AuthWithRoles) UpsertOIDCConnector(connector services.OIDCConnector, ttl time.Duration) error {
if err := a.permChecker.HasPermission(a.role, ActionUpsertOIDCConnector); err != nil {
return trace.Wrap(err)
}
return a.authServer.IdentityService.UpsertOIDCConnector(connector, ttl)
}
func (a *AuthWithRoles) GetOIDCConnector(id string, withSecrets bool) (*services.OIDCConnector, error) {
if withSecrets {
if err := a.permChecker.HasPermission(a.role, ActionGetOIDCConnectorWithSecrets); err != nil {
return nil, trace.Wrap(err)
}
} else {
if err := a.permChecker.HasPermission(a.role, ActionGetOIDCConnectorWithoutSecrets); err != nil {
return nil, trace.Wrap(err)
}
}
return a.authServer.IdentityService.GetOIDCConnector(id, withSecrets)
}
func (a *AuthWithRoles) GetOIDCConnectors(withSecrets bool) ([]services.OIDCConnector, error) {
if withSecrets {
if err := a.permChecker.HasPermission(a.role, ActionGetOIDCConnectorsWithSecrets); err != nil {
return nil, trace.Wrap(err)
}
} else {
if err := a.permChecker.HasPermission(a.role, ActionGetOIDCConnectorsWithoutSecrets); err != nil {
return nil, trace.Wrap(err)
}
}
return a.authServer.IdentityService.GetOIDCConnectors(withSecrets)
}
func (a *AuthWithRoles) CreateOIDCAuthRequest(req services.OIDCAuthRequest) (*services.OIDCAuthRequest, error) {
if err := a.permChecker.HasPermission(a.role, ActionCreateOIDCAuthRequest); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.CreateOIDCAuthRequest(req)
}
func (a *AuthWithRoles) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, error) {
if err := a.permChecker.HasPermission(a.role, ActionValidateOIDCAuthCallback); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.ValidateOIDCAuthCallback(q)
}
func (a *AuthWithRoles) DeleteOIDCConnector(connectorID string) error {
if err := a.permChecker.HasPermission(a.role, ActionDeleteOIDCConnector); err != nil {
return trace.Wrap(err)
}
return a.authServer.IdentityService.DeleteOIDCConnector(connectorID)
}

View file

@ -608,16 +608,12 @@ func (c *Client) GenerateUserCert(
// CreateSignupToken creates one time token for creating account for the user
// For each token it creates username and hotp generator
func (c *Client) CreateSignupToken(user string, allowedLogins []string) (string, error) {
if len(allowedLogins) == 0 {
// TODO(klizhentas) do validation on the serverside
return "", trace.Wrap(
teleport.BadParameter("allowedUsers",
"cannot create a new account without any allowed logins"))
func (c *Client) CreateSignupToken(user services.User) (string, error) {
if err := user.Check(); err != nil {
return "", trace.Wrap(err)
}
out, err := c.PostJSON(c.Endpoint("signuptokens"), createSignupTokenReq{
User: user,
AllowedLogins: allowedLogins,
User: user,
})
if err != nil {
return "", trace.Wrap(err)
@ -663,6 +659,82 @@ func (c *Client) CreateUserWithToken(token, password, hotpToken string) (*Sessio
return sess, nil
}
func (c *Client) UpsertOIDCConnector(connector services.OIDCConnector, ttl time.Duration) error {
_, err := c.PostJSON(c.Endpoint("oidc", "connectors"), upsertOIDCConnectorReq{
Connector: connector,
TTL: ttl,
})
if err != nil {
return trace.Wrap(err)
}
return nil
}
func (c *Client) GetOIDCConnector(id string, withSecrets bool) (*services.OIDCConnector, error) {
if id == "" {
return nil, trace.Wrap(teleport.BadParameter("id", "missing connector id"))
}
out, err := c.Get(c.Endpoint("oidc", "connectors", id),
url.Values{"with_secrets": []string{fmt.Sprintf("%t", withSecrets)}})
if err != nil {
return nil, err
}
var conn *services.OIDCConnector
if err := json.Unmarshal(out.Bytes(), &conn); err != nil {
return nil, trace.Wrap(err)
}
return conn, nil
}
func (c *Client) GetOIDCConnectors(withSecrets bool) ([]services.OIDCConnector, error) {
out, err := c.Get(c.Endpoint("oidc", "connectors"),
url.Values{"with_secrets": []string{fmt.Sprintf("%t", withSecrets)}})
if err != nil {
return nil, err
}
var connectors []services.OIDCConnector
if err := json.Unmarshal(out.Bytes(), &connectors); err != nil {
return nil, trace.Wrap(err)
}
return connectors, nil
}
func (c *Client) DeleteOIDCConnector(connectorID string) error {
if connectorID == "" {
return trace.Wrap(teleport.BadParameter("id", "missing connector id"))
}
_, err := c.Delete(c.Endpoint("oidc", "connectors", connectorID))
return trace.Wrap(err)
}
func (c *Client) CreateOIDCAuthRequest(req services.OIDCAuthRequest) (*services.OIDCAuthRequest, error) {
out, err := c.PostJSON(c.Endpoint("oidc", "requests", "create"), createOIDCAuthRequestReq{
Req: req,
})
if err != nil {
return nil, trace.Wrap(err)
}
var response *services.OIDCAuthRequest
if err := json.Unmarshal(out.Bytes(), &response); err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
func (c *Client) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, error) {
out, err := c.PostJSON(c.Endpoint("oidc", "requests", "validate"), validateOIDCAuthCallbackReq{
Query: q,
})
if err != nil {
return nil, trace.Wrap(err)
}
var response *OIDCAuthResponse
if err := json.Unmarshal(out.Bytes(), &response); err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
type chunkRW struct {
c *Client
id string
@ -743,4 +815,10 @@ type ClientI interface {
GenerateUserCert(key []byte, user string, ttl time.Duration) ([]byte, error)
GetSignupTokenData(token string) (user string, QRImg []byte, hotpFirstValues []string, e error)
CreateUserWithToken(token, password, hotpToken string) (*Session, error)
UpsertOIDCConnector(connector services.OIDCConnector, ttl time.Duration) error
GetOIDCConnector(id string, withSecrets bool) (*services.OIDCConnector, error)
GetOIDCConnectors(withSecrets bool) ([]services.OIDCConnector, error)
DeleteOIDCConnector(connectorID string) error
CreateOIDCAuthRequest(req services.OIDCAuthRequest) (*services.OIDCAuthRequest, error)
ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, error)
}

View file

@ -68,6 +68,10 @@ type InitConfig struct {
// in configuration, so auth server will init the tunnels on the first start
ReverseTunnels []services.ReverseTunnel
// OIDCConnectors is a list of trusted OpenID Connect identity providers
// in configuration, so auth server will init the tunnels on the first start
OIDCConnectors []services.OIDCConnector
// HostCA is an optional host certificate authority keypair
HostCA *services.CertAuthority
@ -193,6 +197,15 @@ func Init(cfg InitConfig) (*AuthServer, *Identity, error) {
}
}
}
if len(cfg.OIDCConnectors) != 0 {
log.Infof("FIRST START: Initializing oidc connectors")
for _, connector := range cfg.OIDCConnectors {
if err := asrv.UpsertOIDCConnector(connector, 0); err != nil {
return nil, nil, trace.Wrap(err)
}
}
}
}
identity, err := initKeys(asrv, cfg.DataDir, IdentityID{HostUUID: cfg.HostUUID, Role: teleport.RoleAdmin})

View file

@ -34,7 +34,6 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/gokyle/hotp"
"github.com/gravitational/configure/cstrings"
"github.com/gravitational/trace"
)
@ -42,19 +41,21 @@ import (
// For each token it creates username and hotp generator
//
// allowedLogins are linux user logins allowed for the new user to use
func (s *AuthServer) CreateSignupToken(user string, allowedLogins []string) (string, error) {
if !cstrings.IsValidUnixUser(user) {
return "", trace.Wrap(
teleport.BadParameter("user", fmt.Sprintf("'%v' is not a valid user name", user)))
func (s *AuthServer) CreateSignupToken(user services.User) (string, error) {
if err := user.Check(); err != nil {
return "", trace.Wrap(err)
}
for _, login := range allowedLogins {
if !cstrings.IsValidUnixUser(login) {
return "", trace.Wrap(teleport.BadParameter(
"allowedLogins", fmt.Sprintf("'%v' is not a valid user name", login)))
// make sure that connectors actually exist
for _, id := range user.OIDCIdentities {
if err := id.Check(); err != nil {
return "", trace.Wrap(err)
}
if _, err := s.GetOIDCConnector(id.ConnectorID, false); err != nil {
return "", trace.Wrap(err)
}
}
// check existing
_, err := s.GetPasswordHash(user)
_, err := s.GetPasswordHash(user.Name)
if err == nil {
return "", trace.Wrap(
teleport.BadParameter(
@ -71,7 +72,7 @@ func (s *AuthServer) CreateSignupToken(user string, allowedLogins []string) (str
log.Errorf("[AUTH API] failed to generate HOTP: %v", err)
return "", trace.Wrap(err)
}
otpQR, err := otp.QR("Teleport: " + user + "@" + s.AuthServiceName)
otpQR, err := otp.QR("Teleport: " + user.Name + "@" + s.AuthServiceName)
if err != nil {
return "", trace.Wrap(err)
}
@ -92,7 +93,6 @@ func (s *AuthServer) CreateSignupToken(user string, allowedLogins []string) (str
Hotp: otpMarshalled,
HotpFirstValues: otpFirstValues,
HotpQR: otpQR,
AllowedLogins: allowedLogins,
}
err = s.UpsertSignupToken(token, tokenData, defaults.MaxSignupTokenTTL)
@ -100,7 +100,7 @@ func (s *AuthServer) CreateSignupToken(user string, allowedLogins []string) (str
return "", trace.Wrap(err)
}
log.Infof("[AUTH API] created the signup token for %v as %v", user, allowedLogins)
log.Infof("[AUTH API] created the signup token for %v as %v", user)
return token, nil
}
@ -125,12 +125,12 @@ func (s *AuthServer) GetSignupTokenData(token string) (user string,
return "", nil, nil, trace.Wrap(err)
}
_, err = s.GetPasswordHash(tokenData.User)
_, err = s.GetPasswordHash(tokenData.User.Name)
if err == nil {
return "", nil, nil, trace.Errorf("can't add user %v, user already exists", tokenData.User)
}
return tokenData.User, tokenData.HotpQR, tokenData.HotpFirstValues, nil
return tokenData.User.Name, tokenData.HotpQR, tokenData.HotpFirstValues, nil
}
// CreateUserWithToken creates account with provided token and password.
@ -164,33 +164,33 @@ func (s *AuthServer) CreateUserWithToken(token, password, hotpToken string) (*Se
return nil, trace.Wrap(teleport.BadParameter("hotp", "wrong HOTP token"))
}
_, _, err = s.UpsertPassword(tokenData.User, []byte(password))
_, _, err = s.UpsertPassword(tokenData.User.Name, []byte(password))
if err != nil {
return nil, trace.Wrap(err)
}
// apply user allowed logins
if err = s.UpsertUser(services.User{Name: tokenData.User, AllowedLogins: tokenData.AllowedLogins}); err != nil {
if err = s.UpsertUser(tokenData.User); err != nil {
return nil, trace.Wrap(err)
}
err = s.UpsertHOTP(tokenData.User, otp)
err = s.UpsertHOTP(tokenData.User.Name, otp)
if err != nil {
return nil, trace.Wrap(err)
}
log.Infof("[AUTH] created new user: %v as %v", tokenData.User, tokenData.AllowedLogins)
log.Infof("[AUTH] created new user: %v", &tokenData.User)
if err = s.DeleteSignupToken(token); err != nil {
return nil, trace.Wrap(err)
}
sess, err := s.NewWebSession(tokenData.User)
sess, err := s.NewWebSession(tokenData.User.Name)
if err != nil {
return nil, trace.Wrap(err)
}
err = s.UpsertWebSession(tokenData.User, sess, WebSessionTTL)
err = s.UpsertWebSession(tokenData.User.Name, sess, WebSessionTTL)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -78,20 +78,22 @@ func NewStandardPermissions() PermissionChecker {
}
sp.permissions[teleport.RoleProxy] = map[string]bool{
ActionGetChunkReader: true,
ActionGetReverseTunnels: true,
ActionGetServers: true,
ActionGetEvents: true,
ActionUpsertProxy: true,
ActionGetProxies: true,
ActionGetAuthServers: true,
ActionGetCertAuthorities: true,
ActionGetUsers: true,
ActionGetLocalDomain: true,
ActionGetUserKeys: true,
ActionLogEntry: true,
ActionGetSession: true,
ActionGetSessions: true,
ActionGetChunkReader: true,
ActionGetReverseTunnels: true,
ActionGetServers: true,
ActionGetEvents: true,
ActionUpsertProxy: true,
ActionGetProxies: true,
ActionGetAuthServers: true,
ActionGetCertAuthorities: true,
ActionGetUsers: true,
ActionGetLocalDomain: true,
ActionGetUserKeys: true,
ActionLogEntry: true,
ActionGetSession: true,
ActionGetSessions: true,
ActionCreateOIDCAuthRequest: true,
ActionValidateOIDCAuthCallback: true,
}
sp.permissions[teleport.RoleWeb] = map[string]bool{
@ -214,4 +216,12 @@ const (
ActionGetSignupTokenData = "GetSignupTokenData"
ActionCreateUserWithToken = "CreateUserWithToken"
ActionUpsertUser = "UpsertUser"
ActionUpsertOIDCConnector = "UpsertOIDCConnector"
ActionDeleteOIDCConnector = "DeleteOIDCConnector"
ActionGetOIDCConnectorWithSecrets = "GetOIDCConnectorWithSecrets"
ActionGetOIDCConnectorWithoutSecrets = "GetOIDCConnectorWithoutSecrets"
ActionGetOIDCConnectorsWithSecrets = "GetOIDCConnectorsWithSecrets"
ActionGetOIDCConnectorsWithoutSecrets = "GetOIDCConnectorsWithoutSecrets"
ActionCreateOIDCAuthRequest = "CreateOIDCAuthRequest"
ActionValidateOIDCAuthCallback = "ValidateOIDCAuthCallback"
)

View file

@ -247,7 +247,7 @@ func (s *TunSuite) TestWebCreatingNewUser(c *C) {
// User will scan QRcode, here we just loads the OTP generator
// right from the backend
tokenData, err := s.a.WebService.GetSignupToken(token)
tokenData, err := s.a.IdentityService.GetSignupToken(token)
c.Assert(err, IsNil)
otp, err := hotp.Unmarshal(tokenData.Hotp)
c.Assert(err, IsNil)
@ -257,7 +257,7 @@ func (s *TunSuite) TestWebCreatingNewUser(c *C) {
hotpTokens[i] = otp.OTP()
}
tokenData3, err := s.a.WebService.GetSignupToken(token3)
tokenData3, err := s.a.IdentityService.GetSignupToken(token3)
c.Assert(err, IsNil)
otp3, err := hotp.Unmarshal(tokenData3.Hotp)
c.Assert(err, IsNil)
@ -292,7 +292,7 @@ func (s *TunSuite) TestWebCreatingNewUser(c *C) {
_, err = clt2.CreateUserWithToken(token, "another_user_signup_attempt", hotpTokens[0])
c.Assert(err, NotNil)
_, err = s.a.WebService.GetSignupToken(token)
_, err = s.a.IdentityService.GetSignupToken(token)
c.Assert(err, NotNil) // token was deleted
// token out of scan range

View file

@ -29,6 +29,7 @@ import (
"github.com/gravitational/teleport"
log "github.com/Sirupsen/logrus"
"github.com/boltdb/bolt"
"github.com/gravitational/trace"
"github.com/mailgun/timetools"
@ -124,6 +125,7 @@ func (b *BoltBackend) CreateVal(bucket []string, key string, val []byte, ttl tim
Value: val,
TTL: ttl,
}
log.Infof("createVal: path=%v, key=%v, ttl=%v", bucket, key, ttl)
bytes, err := json.Marshal(v)
if err != nil {
return trace.Wrap(err)
@ -197,6 +199,7 @@ func (b *BoltBackend) CompareAndSwap(path []string, key string, val []byte, ttl
}
func (b *BoltBackend) GetVal(path []string, key string) ([]byte, error) {
log.Infof("createVal: path=%v key=%v", path, key)
var val []byte
if err := b.getKey(path, key, &val); err != nil {
return nil, trace.Wrap(err)

View file

@ -224,6 +224,15 @@ func ApplyFileConfig(fc *FileConfig, cfg *service.Config) error {
cfg.ReverseTunnels = append(cfg.ReverseTunnels, *tun)
}
// add oidc connectors supplied from configs
for _, c := range fc.Auth.OIDCConnectors {
conn, err := c.Parse()
if err != nil {
return trace.Wrap(err)
}
cfg.OIDCConnectors = append(cfg.OIDCConnectors, *conn)
}
// apply "proxy_service" section
if fc.Proxy.ListenAddress != "" {
addr, err := utils.ParseHostPortAddr(fc.Proxy.ListenAddress, int(defaults.SSHProxyListenPort))

View file

@ -91,6 +91,12 @@ var (
"keys": true,
"reverse_tunnels": true,
"addresses": true,
"oidc_connectors": true,
"id": true,
"issuer_url": true,
"client_id": true,
"client_secret": true,
"redirect_url": true,
}
)
@ -377,9 +383,12 @@ type Auth struct {
// Authorities 3rd party authorities this auth service trusts.
Authorities []Authority `yaml:"authorities,omitempty"`
// List of SSH tunnels to 3rd party proxy services (used to talk
// ReverseTunnels is aist of SSH tunnels to 3rd party proxy services (used to talk
// to 3rd party auth servers we trust)
ReverseTunnels []ReverseTunnel `yaml:"reverse_tunnels,omitempty"`
// OIDCConnectors is a list of trusted OpenID Connect Identity providers
OIDCConnectors []OIDCConnector `yaml:"oidc_connectors"`
}
// SSH is 'ssh_service' section of the config file
@ -516,3 +525,36 @@ func (a *Authority) Parse() (*services.CertAuthority, error) {
return ca, nil
}
// OIDCConnector specifies configuration fo Open ID Connect compatible external
// identity provider, e.g. google in some organisation
type OIDCConnector struct {
// ID is a provider id, 'e.g.' google, used internally
ID string `yaml:"id"`
// Issuer URL is the endpoint of the provider, e.g. https://accounts.google.com
IssuerURL string `yaml:"issuer_url"`
// ClientID is id for authentication client (in our case it's our Auth server)
ClientID string `yaml:"client_id"`
// ClientSecret is used to authenticate our client and should not
// be visible to end user
ClientSecret string `yaml:"client_secret"`
// RedirectURL - Identity provider will use this URL to redirect
// client's browser back to it after successfull authentication
// Should match the URL on Provider's side
RedirectURL string `yaml:"redirect_url"`
}
// Parse parses config struct into services connector and checks if it's valid
func (o *OIDCConnector) Parse() (*services.OIDCConnector, error) {
other := &services.OIDCConnector{
ID: o.ID,
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
RedirectURL: o.RedirectURL,
}
if err := other.Check(); err != nil {
return nil, trace.Wrap(err)
}
return other, nil
}

View file

@ -137,6 +137,9 @@ const (
// ActivePartyTTL is a TTL when party is marked as inactive
ActivePartyTTL = 30 * time.Second
// OIDCAuthRequestTTL is TTL of internally stored auth request created by client
OIDCAuthRequestTTL = 60 * time.Second
)
// Default connection limits, they can be applied separately on any of the Teleport

View file

@ -81,6 +81,9 @@ type Config struct {
// first cluster start
ReverseTunnels []services.ReverseTunnel
// OIDCConnectors is a list of trusted OpenID Connect identity providers
OIDCConnectors []services.OIDCConnector
// PidFile is a full path of the PID file for teleport daemon
PIDFile string
}

View file

@ -286,6 +286,7 @@ func (process *TeleportProcess) initAuthService() error {
HostUUID: cfg.HostUUID,
Authorities: cfg.Auth.Authorities,
ReverseTunnels: cfg.ReverseTunnels,
OIDCConnectors: cfg.OIDCConnectors,
}
authServer, identity, err := auth.Init(acfg)
if err != nil {
@ -579,7 +580,9 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
Proxy: tsrv,
AssetsDir: cfg.Proxy.AssetsDir,
AuthServers: cfg.AuthServers[0],
DomainName: cfg.Hostname})
DomainName: cfg.Hostname,
ProxyClient: conn.Client,
})
if err != nil {
utils.Consolef(cfg.Console, "[PROXY] starting the web server: %v", err)
return trace.Wrap(err)

View file

@ -54,7 +54,7 @@ type ServicesTestSuite struct {
LockS *LockService
PresenceS *PresenceService
ProvisioningS *ProvisioningService
WebS *WebService
WebS *IdentityService
ChangesC chan interface{}
}
@ -64,7 +64,7 @@ func NewServicesTestSuite(backend backend.Backend) *ServicesTestSuite {
s.LockS = NewLockService(backend)
s.PresenceS = NewPresenceService(backend)
s.ProvisioningS = NewProvisioningService(backend)
s.WebS = NewWebService(backend)
s.WebS = NewIdentityService(backend)
s.ChangesC = make(chan interface{})
return &s
}

View file

@ -23,7 +23,7 @@ package services
import (
"encoding/json"
"fmt"
"sync"
"net/url"
"time"
"github.com/gravitational/teleport"
@ -34,6 +34,7 @@ import (
"github.com/gravitational/configure/cstrings"
"github.com/gravitational/trace"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh"
)
// User is an optional user entry in the database
@ -44,6 +45,38 @@ type User struct {
// AllowedLogins represents a list of OS users this teleport
// user is allowed to login as
AllowedLogins []string `json:"allowed_logins"`
// OIDCIdentities lists associated OpenID Connect identities
// that let user log in using externally verified identity
OIDCIdentities []OIDCIdentity `json:"oidc_identities"`
}
func (u *User) String() string {
return fmt.Sprintf("User(name=%v, allowed_logins=%v, identities=%v)", u.Name, u.AllowedLogins, u.OIDCIdentities)
}
// Check checks validity of all parameters
func (u *User) Check() error {
if !cstrings.IsValidUnixUser(u.Name) {
return trace.Wrap(
teleport.BadParameter("Name", fmt.Sprintf("'%v' is not a valid user name", u.Name)))
}
if len(u.AllowedLogins) == 0 {
return trace.Wrap(teleport.BadParameter(
"AllowedLogins", fmt.Sprintf("'%v' has no valid allowed logins", u.Name)))
}
for _, login := range u.AllowedLogins {
if !cstrings.IsValidUnixUser(login) {
return trace.Wrap(teleport.BadParameter(
"login", fmt.Sprintf("'%v' is not a valid user name", login)))
}
}
for _, id := range u.OIDCIdentities {
if err := id.Check(); err != nil {
return trace.Wrap(err)
}
}
return nil
}
// AuthorizedKey is a public key that is authorized to access SSH
@ -55,23 +88,21 @@ type AuthorizedKey struct {
Value []byte `json:"value"`
}
// WebService is responsible for managing web users and currently
// IdentityService is responsible for managing web users and currently
// user accounts as well
type WebService struct {
backend backend.Backend
SignupMutex *sync.Mutex
type IdentityService struct {
backend backend.Backend
}
// NewWebService returns new instance of WebService
func NewWebService(backend backend.Backend) *WebService {
return &WebService{
backend: backend,
SignupMutex: &sync.Mutex{},
// NewIdentityService returns new instance of WebService
func NewIdentityService(backend backend.Backend) *IdentityService {
return &IdentityService{
backend: backend,
}
}
// GetUsers returns a list of users registered with the local auth server
func (s *WebService) GetUsers() ([]User, error) {
func (s *IdentityService) GetUsers() ([]User, error) {
keys, err := s.backend.GetKeys([]string{"web", "users"})
if err != nil {
return nil, trace.Wrap(err)
@ -88,22 +119,29 @@ func (s *WebService) GetUsers() ([]User, error) {
}
// UpsertUser updates parameters about user
func (s *WebService) UpsertUser(user User) error {
func (s *IdentityService) UpsertUser(user User) error {
if !cstrings.IsValidUnixUser(user.Name) {
return trace.Wrap(
teleport.BadParameter("user.Name", fmt.Sprintf("'%v is not a valid unix username'", user.Name)))
}
data, err := json.Marshal(user.AllowedLogins)
if err != nil {
return trace.Wrap(err)
}
for _, l := range user.AllowedLogins {
if !cstrings.IsValidUnixUser(l) {
return trace.Wrap(
teleport.BadParameter("login", fmt.Sprintf("'%v is not a valid unix username'", l)))
}
}
err = s.backend.UpsertVal([]string{"web", "users", user.Name}, "logins", []byte(data), backend.Forever)
for _, i := range user.OIDCIdentities {
if err := i.Check(); err != nil {
return trace.Wrap(err)
}
}
data, err := json.Marshal(user)
if err != nil {
return trace.Wrap(err)
}
err = s.backend.UpsertVal([]string{"web", "users", user.Name}, "params", []byte(data), backend.Forever)
if err != nil {
return trace.Wrap(err)
}
@ -111,23 +149,40 @@ func (s *WebService) UpsertUser(user User) error {
}
// GetUser returns a user by name
func (s *WebService) GetUser(user string) (*User, error) {
func (s *IdentityService) GetUser(user string) (*User, error) {
u := User{Name: user}
data, err := s.backend.GetVal([]string{"web", "users", user}, "logins")
data, err := s.backend.GetVal([]string{"web", "users", user}, "params")
if err != nil {
if teleport.IsNotFound(err) {
return &u, nil
}
return nil, trace.Wrap(err)
}
if err := json.Unmarshal(data, &u.AllowedLogins); err != nil {
if err := json.Unmarshal(data, &u); err != nil {
return nil, trace.Wrap(err)
}
return &u, nil
}
// GetUserByOIDCIdentity returns a user by it's specified OIDC Identity, returns first
// user specified with this identity
func (s *IdentityService) GetUserByOIDCIdentity(id OIDCIdentity) (*User, error) {
users, err := s.GetUsers()
if err != nil {
return nil, trace.Wrap(err)
}
for _, u := range users {
for _, uid := range u.OIDCIdentities {
if uid.Equals(&id) {
return &u, nil
}
}
}
return nil, trace.Wrap(teleport.NotFound(fmt.Sprintf("user with identity %v not found", &id)))
}
// DeleteUser deletes a user with all the keys from the backend
func (s *WebService) DeleteUser(user string) error {
func (s *IdentityService) DeleteUser(user string) error {
err := s.backend.DeleteBucket([]string{"web", "users"}, user)
if err != nil {
if teleport.IsNotFound(err) {
@ -138,7 +193,7 @@ func (s *WebService) DeleteUser(user string) error {
}
// UpsertPasswordHash upserts user password hash
func (s *WebService) UpsertPasswordHash(user string, hash []byte) error {
func (s *IdentityService) UpsertPasswordHash(user string, hash []byte) error {
err := s.backend.UpsertVal([]string{"web", "users", user}, "pwd", hash, 0)
if err != nil {
return trace.Wrap(err)
@ -147,7 +202,7 @@ func (s *WebService) UpsertPasswordHash(user string, hash []byte) error {
}
// GetPasswordHash returns the password hash for a given user
func (s *WebService) GetPasswordHash(user string) ([]byte, error) {
func (s *IdentityService) GetPasswordHash(user string) ([]byte, error) {
hash, err := s.backend.GetVal([]string{"web", "users", user}, "pwd")
if err != nil {
if teleport.IsNotFound(err) {
@ -159,7 +214,7 @@ func (s *WebService) GetPasswordHash(user string) ([]byte, error) {
}
// UpsertHOTP upserts HOTP state for user
func (s *WebService) UpsertHOTP(user string, otp *hotp.HOTP) error {
func (s *IdentityService) UpsertHOTP(user string, otp *hotp.HOTP) error {
bytes, err := hotp.Marshal(otp)
if err != nil {
return trace.Wrap(err)
@ -173,7 +228,7 @@ func (s *WebService) UpsertHOTP(user string, otp *hotp.HOTP) error {
}
// GetHOTP gets HOTP token state for a user
func (s *WebService) GetHOTP(user string) (*hotp.HOTP, error) {
func (s *IdentityService) GetHOTP(user string) (*hotp.HOTP, error) {
bytes, err := s.backend.GetVal([]string{"web", "users", user},
"hotp")
if err != nil {
@ -190,7 +245,7 @@ func (s *WebService) GetHOTP(user string) (*hotp.HOTP, error) {
}
// UpsertWebSession updates or inserts a web session for a user and session id
func (s *WebService) UpsertWebSession(user, sid string, session WebSession, ttl time.Duration) error {
func (s *IdentityService) UpsertWebSession(user, sid string, session WebSession, ttl time.Duration) error {
bytes, err := json.Marshal(session)
if err != nil {
return trace.Wrap(err)
@ -204,7 +259,7 @@ func (s *WebService) UpsertWebSession(user, sid string, session WebSession, ttl
}
// GetWebSession returns a web session state for a given user and session id
func (s *WebService) GetWebSession(user, sid string) (*WebSession, error) {
func (s *IdentityService) GetWebSession(user, sid string) (*WebSession, error) {
val, err := s.backend.GetVal(
[]string{"web", "users", user, "sessions"},
sid,
@ -223,7 +278,7 @@ func (s *WebService) GetWebSession(user, sid string) (*WebSession, error) {
}
// GetWebSessionsKeys returns public keys associated with the session
func (s *WebService) GetWebSessionsKeys(user string) ([]AuthorizedKey, error) {
func (s *IdentityService) GetWebSessionsKeys(user string) ([]AuthorizedKey, error) {
keys, err := s.backend.GetKeys([]string{"web", "users", user, "sessions"})
if err != nil {
return nil, err
@ -241,7 +296,7 @@ func (s *WebService) GetWebSessionsKeys(user string) ([]AuthorizedKey, error) {
}
// DeleteWebSession deletes web session from the storage
func (s *WebService) DeleteWebSession(user, sid string) error {
func (s *IdentityService) DeleteWebSession(user, sid string) error {
err := s.backend.DeleteKey(
[]string{"web", "users", user, "sessions"},
sid,
@ -250,7 +305,7 @@ func (s *WebService) DeleteWebSession(user, sid string) error {
}
// UpsertPassword upserts new password and HOTP token
func (s *WebService) UpsertPassword(user string,
func (s *IdentityService) UpsertPassword(user string,
password []byte) (hotpURL string, hotpQR []byte, err error) {
if err := verifyPassword(password); err != nil {
@ -288,7 +343,7 @@ func (s *WebService) UpsertPassword(user string,
}
// CheckPassword is called on web user or tsh user login
func (s *WebService) CheckPassword(user string, password []byte, hotpToken string) error {
func (s *IdentityService) CheckPassword(user string, password []byte, hotpToken string) error {
if err := verifyPassword(password); err != nil {
return trace.Wrap(err)
}
@ -314,7 +369,7 @@ func (s *WebService) CheckPassword(user string, password []byte, hotpToken strin
// CheckPasswordWOToken checks just password without checking HOTP tokens
// used in case of SSH authentication, when token has been validated
func (s *WebService) CheckPasswordWOToken(user string, password []byte) error {
func (s *IdentityService) CheckPasswordWOToken(user string, password []byte) error {
if err := verifyPassword(password); err != nil {
return trace.Wrap(err)
}
@ -347,6 +402,129 @@ func verifyPassword(password []byte) error {
return nil
}
// UpsertSignupToken upserts signup token - one time token that lets user to create a user account
func (s *IdentityService) UpsertSignupToken(token string, tokenData SignupToken, ttl time.Duration) error {
if ttl < time.Second || ttl > defaults.MaxSignupTokenTTL {
ttl = defaults.MaxSignupTokenTTL
}
out, err := json.Marshal(tokenData)
if err != nil {
return trace.Wrap(err)
}
err = s.backend.UpsertVal(userTokensPath, token, out, ttl)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// GetSignupToken returns signup token data
func (s *IdentityService) GetSignupToken(token string) (*SignupToken, error) {
out, err := s.backend.GetVal(userTokensPath, token)
if err != nil {
return nil, trace.Wrap(err)
}
var data *SignupToken
err = json.Unmarshal(out, &data)
if err != nil {
return nil, trace.Wrap(err)
}
return data, nil
}
// DeleteSignupToken deletes signup token from the storage
func (s *IdentityService) DeleteSignupToken(token string) error {
err := s.backend.DeleteKey(userTokensPath, token)
return trace.Wrap(err)
}
// UpsertOIDCConnector upserts OIDC Connector
func (s *IdentityService) UpsertOIDCConnector(connector OIDCConnector, ttl time.Duration) error {
if err := connector.Check(); err != nil {
return trace.Wrap(err)
}
data, err := json.Marshal(connector)
if err != nil {
return trace.Wrap(err)
}
err = s.backend.UpsertVal(connectorsPath, connector.ID, data, ttl)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// DeleteOIDCConnector deletes OIDC Connector
func (s *IdentityService) DeleteOIDCConnector(connectorID string) error {
err := s.backend.DeleteKey(connectorsPath, connectorID)
return trace.Wrap(err)
}
// GetOIDCConnector returns OIDC connector data, , withSecrets adds or removes client secret from return results
func (s *IdentityService) GetOIDCConnector(id string, withSecrets bool) (*OIDCConnector, error) {
out, err := s.backend.GetVal(connectorsPath, id)
if err != nil {
return nil, trace.Wrap(err)
}
var data *OIDCConnector
err = json.Unmarshal(out, &data)
if err != nil {
return nil, trace.Wrap(err)
}
if !withSecrets {
data.ClientSecret = ""
}
return data, nil
}
// GetOIDCConnectors returns registered connectors, withSecrets adds or removes client secret from return results
func (s *IdentityService) GetOIDCConnectors(withSecrets bool) ([]OIDCConnector, error) {
connectorIDs, err := s.backend.GetKeys(connectorsPath)
if err != nil {
return nil, trace.Wrap(err)
}
connectors := make([]OIDCConnector, 0, len(connectorIDs))
for _, id := range connectorIDs {
connector, err := s.GetOIDCConnector(id, withSecrets)
if err != nil {
return nil, trace.Wrap(err)
}
connectors = append(connectors, *connector)
}
return connectors, nil
}
// CreateOIDCAuthRequest creates new auth request
func (s *IdentityService) CreateOIDCAuthRequest(req OIDCAuthRequest, ttl time.Duration) error {
if err := req.Check(); err != nil {
return trace.Wrap(err)
}
data, err := json.Marshal(req)
if err != nil {
return trace.Wrap(err)
}
err = s.backend.CreateVal(authRequestsPath, req.StateToken, data, ttl)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// GetOIDCAuthRequest returns OIDC auth request if found
func (s *IdentityService) GetOIDCAuthRequest(stateToken string) (*OIDCAuthRequest, error) {
data, err := s.backend.GetVal(authRequestsPath, stateToken)
if err != nil {
return nil, trace.Wrap(err)
}
var req *OIDCAuthRequest
if err := json.Unmarshal(data, &req); err != nil {
return nil, trace.Wrap(err)
}
return req, nil
}
// WebSession stores key and value used to authenticate with SSH
// notes on behalf of user
type WebSession struct {
@ -365,51 +543,135 @@ type WebSession struct {
// is stored and generated when tctl add user is executed
type SignupToken struct {
Token string `json:"token"`
User string `json:"user"`
User User `json:"user"`
Hotp []byte `json:"hotp"`
HotpFirstValues []string `json:"hotp_first_values"`
HotpQR []byte `json:"hotp_qr"`
AllowedLogins []string `json:"allowed_logins"`
}
// OIDCConnector specifies configuration fo Open ID Connect compatible external
// identity provider, e.g. google in some organisation
type OIDCConnector struct {
// ID is a provider id, 'e.g.' google, used internally
ID string `json:"id"`
// Issuer URL is the endpoint of the provider, e.g. https://accounts.google.com
IssuerURL string `json:"issuer_url"`
// ClientID is id for authentication client (in our case it's our Auth server)
ClientID string `json:"client_id"`
// ClientSecret is used to authenticate our client and should not
// be visible to end user
ClientSecret string `json:"client_secret"`
// RedirectURL - Identity provider will use this URL to redirect
// client's browser back to it after successfull authentication
// Should match the URL on Provider's side
RedirectURL string `json:"redirect_url"`
}
// Check returns nil if all parameters are great, err otherwise
func (o *OIDCConnector) Check() error {
if o.ID == "" {
return trace.Wrap(teleport.BadParameter("ID", "missing connector id"))
}
if _, err := url.Parse(o.IssuerURL); err != nil {
return trace.Wrap(teleport.BadParameter("IssuerURL", fmt.Sprintf("bad url: '%v'", o.IssuerURL)))
}
if _, err := url.Parse(o.RedirectURL); err != nil {
return trace.Wrap(teleport.BadParameter("RedirectURL", fmt.Sprintf("bad url: '%v'", o.RedirectURL)))
}
if o.ClientID == "" {
return trace.Wrap(teleport.BadParameter("ClientID", "missing client id"))
}
if o.ClientSecret == "" {
return trace.Wrap(teleport.BadParameter("ClientID", "missing client secret"))
}
return nil
}
var (
userTokensPath = []string{"addusertokens"}
userTokensPath = []string{"addusertokens"}
connectorsPath = []string{"web", "connectors", "oidc", "connectors"}
authRequestsPath = []string{"web", "connectors", "oidc", "requests"}
)
// UpsertSignupToken upserts signup token - one time token that lets user to create a user account
func (s *WebService) UpsertSignupToken(token string, tokenData SignupToken, ttl time.Duration) error {
if ttl < time.Second || ttl > defaults.MaxSignupTokenTTL {
ttl = defaults.MaxSignupTokenTTL
}
out, err := json.Marshal(tokenData)
if err != nil {
return trace.Wrap(err)
}
// OIDCIdentity is OpenID Connect identity that is linked
// to particular user and connector and lets user to log in using external
// credentials, e.g. google
type OIDCIdentity struct {
// ConnectorID is id of registered OIDC connector, e.g. 'google-example.com'
ConnectorID string `json:"connector_id"`
err = s.backend.UpsertVal(userTokensPath, token, out, ttl)
if err != nil {
return trace.Wrap(err)
// Email is OIDC verified email claim
// e.g. bob@example.com
Email string `json:"username"`
}
// String returns debug friendly representation of this identity
func (i *OIDCIdentity) String() string {
return fmt.Sprintf("OIDCIdentity(connectorID=%v, email=%v)", i.ConnectorID, i.Email)
}
// Equals returns true if this identity equals to passed one
func (i *OIDCIdentity) Equals(other *OIDCIdentity) bool {
return i.ConnectorID == other.ConnectorID && i.Email == other.Email
}
// Check returns nil if all parameters are great, err otherwise
func (i *OIDCIdentity) Check() error {
if i.ConnectorID == "" {
return trace.Wrap(teleport.BadParameter("ConnectorID", "missing value"))
}
if i.Email == "" {
return trace.Wrap(teleport.BadParameter("Email", "missing email"))
}
return nil
}
// GetSignupToken returns signup token data
func (s *WebService) GetSignupToken(token string) (*SignupToken, error) {
out, err := s.backend.GetVal(userTokensPath, token)
if err != nil {
return nil, trace.Wrap(err)
// OIDCAuthRequest is a request to authenticate with OIDC
// provider, the state about request is managed by auth server
type OIDCAuthRequest struct {
// ConnectorID is ID of OIDC connector this request uses
ConnectorID string `json:"connector_id"`
// StateToken is generated by service and is used to validate
// reuqest coming from
StateToken string `json:"state_token"`
// ClientStateToken is passed by console client
ClientStateToken string `json:"client_state_token"`
// RedirectURL will be used by browser
RedirectURL string `json:"redirect_url"`
// PublicKey is an optional public key, users want these
// keys to be signed by auth servers user CA in case
// of successfull auth
PublicKey []byte `json:"public_key"`
// CertTTL is the TTL of the certificate user wants to get
CertTTL time.Duration `json:"cert_ttl"`
// CreateWebSession indicates if user wants to generate a web
// session after successful authentication
CreateWebSession bool `json:"create_web_session"`
}
// Check returns nil if all parameters are great, err otherwise
func (i *OIDCAuthRequest) Check() error {
if i.ConnectorID == "" {
return trace.Wrap(teleport.BadParameter("ConnectorID", "missing value"))
}
var data *SignupToken
err = json.Unmarshal(out, &data)
if err != nil {
return nil, trace.Wrap(err)
if i.StateToken == "" {
return trace.Wrap(teleport.BadParameter("StateToken", "missing value"))
}
if len(i.PublicKey) != 0 {
_, _, _, _, err := ssh.ParseAuthorizedKey(i.PublicKey)
if err != nil {
return trace.Wrap(teleport.BadParameter("PublicKey", fmt.Sprintf("bad key: %v", err)))
}
if (i.CertTTL > defaults.MaxCertDuration) || (i.CertTTL < defaults.MinCertDuration) {
return trace.Wrap(teleport.BadParameter("CertTTL", "wrong certificate TTL"))
}
}
return data, nil
}
// DeleteSignupToken deletes signup token from the storage
func (s *WebService) DeleteSignupToken(token string) error {
err := s.backend.DeleteKey(userTokensPath, token)
return trace.Wrap(err)
return nil
}

View file

@ -48,11 +48,11 @@ import (
// Handler is HTTP web proxy handler
type Handler struct {
httprouter.Router
cfg Config
auth *sessionCache
sites *ttlmap.TtlMap
sync.Mutex
httprouter.Router
cfg Config
auth *sessionCache
sites *ttlmap.TtlMap
sessionStreamPollPeriod time.Duration
}
@ -85,6 +85,8 @@ type Config struct {
AuthServers utils.NetAddr
// DomainName is a domain name served by web handler
DomainName string
// ProxyClient is a client that authenticated as proxy
ProxyClient auth.ClientI
}
// Version is a current webapi version
@ -153,6 +155,10 @@ func NewHandler(cfg Config, opts ...HandlerOption) (http.Handler, error) {
// get session chunks count
h.GET("/webapi/sites/:site/sessions/:sid/chunkscount", h.withSiteAuth(h.siteSessionGetChunksCount))
// OIDC related callback handlers
h.GET("/webapi/oidc/login", httplib.MakeHandler(h.oidcLogin))
h.GET("/webapi/oidc/callback", httplib.MakeHandler(h.oidcCallback))
routingHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/web", http.StatusFound)
@ -168,6 +174,29 @@ func NewHandler(cfg Config, opts ...HandlerOption) (http.Handler, error) {
return routingHandler, nil
}
func (m *Handler) oidcLogin(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Infof("oidcLogin start")
response, err := m.cfg.ProxyClient.CreateOIDCAuthRequest(
services.OIDCAuthRequest{
ConnectorID: "google",
CreateWebSession: true,
})
if err != nil {
return nil, trace.Wrap(err)
}
http.Redirect(w, r, response.RedirectURL, http.StatusFound)
return nil, nil
}
func (m *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Infof("oidcLogin validate")
response, err := m.cfg.ProxyClient.ValidateOIDCAuthCallback(r.URL.Query())
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
// createSessionReq is a request to create session from username, password and second
// factor token
type createSessionReq struct {

View file

@ -51,6 +51,7 @@ type UserCommand struct {
config *service.Config
login string
allowedLogins string
identity string
}
type NodeCommand struct {
@ -113,6 +114,8 @@ func main() {
userAdd.Arg("login", "Teleport user login").Required().StringVar(&cmdUsers.login)
userAdd.Arg("local-logins", "Local UNIX users this account can log in as [login]").
Default("").StringVar(&cmdUsers.allowedLogins)
userAdd.Arg("external-auth", "External authentication methods, e.g. google:bob@gmail.com for google SSO").
Default("").StringVar(&cmdUsers.identity)
userAdd.Alias(AddUserHelp)
// list users command
@ -256,7 +259,18 @@ func (u *UserCommand) Add(hostname string, client *auth.TunClient) error {
if u.allowedLogins == "" {
u.allowedLogins = u.login
}
token, err := client.CreateSignupToken(u.login, strings.Split(u.allowedLogins, ","))
user := services.User{
Name: u.login,
AllowedLogins: strings.Split(u.allowedLogins, ","),
}
if u.identity != "" {
vals := strings.Split(u.identity, ":")
if len(vals) != 2 {
return trace.Errorf("expected connector:email for external auth method, e.g. google:alice@gmail.com")
}
user.OIDCIdentities = []services.OIDCIdentity{{ConnectorID: vals[0], Email: vals[1]}}
}
token, err := client.CreateSignupToken(user)
if err != nil {
return err
}