diff --git a/src/github.com/matrix-org/go-neb/api.go b/src/github.com/matrix-org/go-neb/api.go index 620fc56..6904a85 100644 --- a/src/github.com/matrix-org/go-neb/api.go +++ b/src/github.com/matrix-org/go-neb/api.go @@ -33,6 +33,10 @@ func (h *requestAuthSessionHandler) OnIncomingRequest(req *http.Request) (interf if err := json.NewDecoder(req.Body).Decode(&body); err != nil { return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} } + log.WithFields(log.Fields{ + "realm_id": body.RealmID, + "user_id": body.UserID, + }).Print("Incoming auth session request") if body.UserID == "" || body.RealmID == "" || body.Config == nil { return nil, &errors.HTTPError{nil, `Must supply a "UserID", a "RealmID" and a "Config"`, 400} @@ -65,6 +69,9 @@ func (rh *realmRedirectHandler) handle(w http.ResponseWriter, req *http.Request) w.WriteHeader(404) return } + log.WithFields(log.Fields{ + "realm_id": realmID, + }).Print("Incoming realm redirect request") realm.OnReceiveRedirect(w, req) } diff --git a/src/github.com/matrix-org/go-neb/realms/github/github.go b/src/github.com/matrix-org/go-neb/realms/github/github.go index f192495..d6e3ef0 100644 --- a/src/github.com/matrix-org/go-neb/realms/github/github.go +++ b/src/github.com/matrix-org/go-neb/realms/github/github.go @@ -13,10 +13,10 @@ import ( ) type githubRealm struct { - id string - ClientSecret string - ClientID string - RedirectBaseURI string + id string + redirectURL string + ClientSecret string + ClientID string } // GithubSession represents an authenticated github session @@ -68,8 +68,7 @@ func (r *githubRealm) RequestAuthSession(userID string, req json.RawMessage) int q.Set("client_id", r.ClientID) q.Set("client_secret", r.ClientSecret) q.Set("state", state) - // TODO: Path is from goneb.go - we should probably factor it out. - q.Set("redirect_uri", r.RedirectBaseURI+"/realms/redirects/"+r.ID()) + q.Set("redirect_uri", r.redirectURL) u.RawQuery = q.Encode() session := &GithubSession{ id: state, // key off the state for redirects @@ -171,7 +170,7 @@ func randomString(length int) (string, error) { } func init() { - types.RegisterAuthRealm(func(realmID string) types.AuthRealm { - return &githubRealm{id: realmID} + types.RegisterAuthRealm(func(realmID, redirectURL string) types.AuthRealm { + return &githubRealm{id: realmID, redirectURL: redirectURL} }) } diff --git a/src/github.com/matrix-org/go-neb/realms/jira/jira.go b/src/github.com/matrix-org/go-neb/realms/jira/jira.go index 50cc6b2..afbba47 100644 --- a/src/github.com/matrix-org/go-neb/realms/jira/jira.go +++ b/src/github.com/matrix-org/go-neb/realms/jira/jira.go @@ -18,6 +18,7 @@ import ( type jiraRealm struct { id string + redirectURL string privateKey *rsa.PrivateKey JIRAEndpoint string Server string // clobbered based on /serverInfo request @@ -29,21 +30,29 @@ type jiraRealm struct { PrivateKeyPEM string } +// JIRASession represents a single authentication session between a user and a JIRA endpoint. +// The endpoint is dictated by the realm ID. type JIRASession struct { - id string // request token - userID string - realmID string - Secret string // request secret + id string // request token + userID string + realmID string + RequestSecret string + AccessToken string + AccessSecret string } +// UserID returns the ID of the user performing the authentication. func (s *JIRASession) UserID() string { return s.userID } +// RealmID returns the JIRA realm ID which created this session. func (s *JIRASession) RealmID() string { return s.realmID } +// ID returns the OAuth1 request_token which is used when looking up sessions in the redirect +// handler. func (s *JIRASession) ID() string { return s.id } @@ -64,21 +73,13 @@ func (r *jiraRealm) Register() error { return errors.New("JIRAEndpoint must be specified") } - // Make sure the private key PEM is actually a private key. - err := r.parsePrivateKey() + err := r.ensureInited() if err != nil { return err } - // Parse the messy input URL into a canonicalised form. - ju, err := urls.ParseJIRAURL(r.JIRAEndpoint) - if err != nil { - return err - } - r.JIRAEndpoint = ju.Base - // Check to see if JIRA endpoint is valid by pinging an endpoint - cli, err := r.jiraClient(ju, "", true) + cli, err := r.jiraClient(r.JIRAEndpoint, "", true) if err != nil { return err } @@ -87,7 +88,7 @@ func (r *jiraRealm) Register() error { return err } log.WithFields(log.Fields{ - "jira_url": ju.Base, + "jira_url": r.JIRAEndpoint, "title": info.ServerTitle, "version": info.Version, }).Print("Found JIRA endpoint") @@ -98,19 +99,13 @@ func (r *jiraRealm) Register() error { } func (r *jiraRealm) RequestAuthSession(userID string, req json.RawMessage) interface{} { + err := r.ensureInited() logger := log.WithField("jira_url", r.JIRAEndpoint) - // Parse the private key as we may not have called Register() - err := r.parsePrivateKey() - if err != nil { - logger.WithError(err).Print("Failed to parse private key") - return nil - } - ju, err := urls.ParseJIRAURL(r.JIRAEndpoint) if err != nil { - log.WithError(err).Print("Failed to parse JIRA endpoint") + logger.WithError(err).Print("Failed to init realm") return nil } - authConfig := r.oauth1Config(ju) + authConfig := r.oauth1Config(r.JIRAEndpoint) reqToken, reqSec, err := authConfig.RequestToken() if err != nil { logger.WithError(err).Print("Failed to request auth token") @@ -124,10 +119,10 @@ func (r *jiraRealm) RequestAuthSession(userID string, req json.RawMessage) inter } _, err = database.GetServiceDB().StoreAuthSession(&JIRASession{ - id: reqToken, - userID: userID, - realmID: r.id, - Secret: reqSec, + id: reqToken, + userID: userID, + realmID: r.id, + RequestSecret: reqSec, }) if err != nil { log.WithError(err).Print("Failed to store new auth session") @@ -140,16 +135,65 @@ func (r *jiraRealm) RequestAuthSession(userID string, req json.RawMessage) inter } func (r *jiraRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request) { + err := r.ensureInited() + logger := log.WithField("jira_url", r.JIRAEndpoint) + if err != nil { + failWith(logger, w, 500, "Failed to initialise realm", err) + return + } + + requestToken, verifier, err := oauth1.ParseAuthorizationCallback(req) + if err != nil { + failWith(logger, w, 400, "Failed to parse authorization callback", err) + return + } + logger = logger.WithField("req_token", requestToken) + logger.Print("Received authorization callback") + + session, err := database.GetServiceDB().LoadAuthSessionByID(r.id, requestToken) + if err != nil { + failWith(logger, w, 400, "Unrecognised request token", err) + return + } + jiraSession, ok := session.(*JIRASession) + if !ok { + failWith(logger, w, 500, "Unexpected session type found.", nil) + return + } + logger = logger.WithField("user_id", jiraSession.UserID()) + logger.Print("Retrieved auth session for user") + + oauthConfig := r.oauth1Config(r.JIRAEndpoint) + accessToken, accessSecret, err := oauthConfig.AccessToken(requestToken, jiraSession.RequestSecret, verifier) + if err != nil { + failWith(logger, w, 502, "Failed exchange for access token.", err) + return + } + logger.Print("Exchanged for access token") + jiraSession.AccessToken = accessToken + jiraSession.AccessSecret = accessSecret + + _, err = database.GetServiceDB().StoreAuthSession(jiraSession) + if err != nil { + failWith(logger, w, 500, "Failed to persist JIRA session", err) + return + } + w.WriteHeader(200) + w.Write([]byte("OK!")) } func (r *jiraRealm) AuthSession(id, userID, realmID string) types.AuthSession { - return nil + return &JIRASession{ + id: id, + userID: userID, + realmID: realmID, + } } // jiraClient returns an authenticated jira.Client for the given userID. Returns an unauthenticated // client if allowUnauth is true and no authenticated session is found, else returns an error. -func (r *jiraRealm) jiraClient(u urls.JIRAURL, userID string, allowUnauth bool) (*jira.Client, error) { +func (r *jiraRealm) jiraClient(jiraBaseURL, userID string, allowUnauth bool) (*jira.Client, error) { // TODO: Check if user has an auth session. Requires access token+secret hasAuthSession := false @@ -157,21 +201,40 @@ func (r *jiraRealm) jiraClient(u urls.JIRAURL, userID string, allowUnauth bool) // make an authenticated client var cli *jira.Client - auth := r.oauth1Config(u) + auth := r.oauth1Config(jiraBaseURL) httpClient := auth.Client(context.TODO(), oauth1.NewToken("access_tokenTODO", "access_secretTODO")) - cli, err := jira.NewClient(httpClient, u.Base) + cli, err := jira.NewClient(httpClient, jiraBaseURL) return cli, err } else if allowUnauth { // make an unauthenticated client - cli, err := jira.NewClient(nil, u.Base) + cli, err := jira.NewClient(nil, jiraBaseURL) return cli, err } else { return nil, errors.New("No authenticated session found for " + userID) } } +func (r *jiraRealm) ensureInited() error { + err := r.parsePrivateKey() + if err != nil { + log.WithError(err).Print("Failed to parse private key") + return err + } + // Parse the messy input URL into a canonicalised form. + ju, err := urls.ParseJIRAURL(r.JIRAEndpoint) + if err != nil { + log.WithError(err).Print("Failed to parse JIRA endpoint") + return err + } + r.JIRAEndpoint = ju.Base + return nil +} + func (r *jiraRealm) parsePrivateKey() error { + if r.privateKey != nil { + return nil + } pk, err := loadPrivateKey(r.PrivateKeyPEM) if err != nil { return err @@ -185,21 +248,20 @@ func (r *jiraRealm) parsePrivateKey() error { return nil } -func (r *jiraRealm) oauth1Config(u urls.JIRAURL) *oauth1.Config { +func (r *jiraRealm) oauth1Config(jiraBaseURL string) *oauth1.Config { return &oauth1.Config{ ConsumerKey: r.ConsumerKey, ConsumerSecret: r.ConsumerSecret, - // TODO: path from goneb.go - we should factor it out like we did with Services - CallbackURL: u.Base + "realms/redirect/" + r.id, + CallbackURL: r.redirectURL, // TODO: In JIRA Cloud, the Authorization URL is only the Instance BASE_URL: // https://BASE_URL.atlassian.net. // It also does not require the + "/plugins/servlet/oauth/authorize" // We should probably check the provided JIRA base URL to see if it is a cloud one // then adjust accordingly. Endpoint: oauth1.Endpoint{ - RequestTokenURL: u.Base + "plugins/servlet/oauth/request-token", - AuthorizeURL: u.Base + "plugins/servlet/oauth/authorize", - AccessTokenURL: u.Base + "plugins/servlet/oauth/access-token", + RequestTokenURL: jiraBaseURL + "plugins/servlet/oauth/request-token", + AuthorizeURL: jiraBaseURL + "plugins/servlet/oauth/authorize", + AccessTokenURL: jiraBaseURL + "plugins/servlet/oauth/access-token", }, Signer: &oauth1.RSASigner{ PrivateKey: r.privateKey, @@ -253,8 +315,15 @@ func jiraServerInfo(cli *jira.Client) (*jiraServiceInfo, error) { return &jsi, nil } +// TODO: Github has this as well, maybe factor it out? +func failWith(logger *log.Entry, w http.ResponseWriter, code int, msg string, err error) { + logger.WithError(err).Print(msg) + w.WriteHeader(code) + w.Write([]byte(msg)) +} + func init() { - types.RegisterAuthRealm(func(realmID string) types.AuthRealm { - return &jiraRealm{id: realmID} + types.RegisterAuthRealm(func(realmID, redirectURL string) types.AuthRealm { + return &jiraRealm{id: realmID, redirectURL: redirectURL} }) } diff --git a/src/github.com/matrix-org/go-neb/types/types.go b/src/github.com/matrix-org/go-neb/types/types.go index c010064..da9a00e 100644 --- a/src/github.com/matrix-org/go-neb/types/types.go +++ b/src/github.com/matrix-org/go-neb/types/types.go @@ -87,11 +87,11 @@ type AuthRealm interface { RequestAuthSession(userID string, config json.RawMessage) interface{} } -var realmsByType = map[string]func(string) AuthRealm{} +var realmsByType = map[string]func(string, string) AuthRealm{} // RegisterAuthRealm registers a factory for creating AuthRealm instances. -func RegisterAuthRealm(factory func(string) AuthRealm) { - realmsByType[factory("").Type()] = factory +func RegisterAuthRealm(factory func(string, string) AuthRealm) { + realmsByType[factory("", "").Type()] = factory } // CreateAuthRealm creates an AuthRealm of the given type and realm ID. @@ -101,7 +101,8 @@ func CreateAuthRealm(realmID, realmType string) AuthRealm { if f == nil { return nil } - return f(realmID) + redirectURL := baseURL + "realms/redirects/" + realmID + return f(realmID, redirectURL) } // AuthSession represents a single authentication session between a user and