Browse Source

Merge pull request #17 from matrix-org/kegan/jira-auth-sessions

Implement JIRA OAuth1 flow
pull/20/head
Kegsay 8 years ago
committed by GitHub
parent
commit
a15312e7a0
  1. 17
      README.md
  2. 7
      src/github.com/matrix-org/go-neb/api.go
  3. 9
      src/github.com/matrix-org/go-neb/realms/github/github.go
  4. 203
      src/github.com/matrix-org/go-neb/realms/jira/jira.go
  5. 9
      src/github.com/matrix-org/go-neb/types/types.go

17
README.md

@ -199,7 +199,22 @@ JIRA installation. Once that is complete, users can OAuth on the target JIRA ins
### Make a request for JIRA Auth
TODO
```
curl -X POST localhost:4050/admin/requestAuthSession --data-binary '{
"RealmID": "jirarealm",
"UserID": "@your_user_id:localhost",
"Config": {
}
}'
```
Returns:
```json
{
"URL":"https://jira.somewhere.com/plugins/servlet/oauth/authorize?oauth_token=7yeuierbgweguiegrTbOT"
}
```
Follow this link and grant access for NEB to act on your behalf.
### Create a JIRA bot

7
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)
}

9
src/github.com/matrix-org/go-neb/realms/github/github.go

@ -14,9 +14,9 @@ import (
type githubRealm struct {
id string
redirectURL string
ClientSecret string
ClientID string
RedirectBaseURI 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}
})
}

203
src/github.com/matrix-org/go-neb/realms/jira/jira.go

@ -9,6 +9,7 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/andygrunwald/go-jira"
"github.com/dghubble/oauth1"
"github.com/matrix-org/go-neb/database"
"github.com/matrix-org/go-neb/realms/jira/urls"
"github.com/matrix-org/go-neb/types"
"golang.org/x/net/context"
@ -17,6 +18,7 @@ import (
type jiraRealm struct {
id string
redirectURL string
privateKey *rsa.PrivateKey
JIRAEndpoint string
Server string // clobbered based on /serverInfo request
@ -28,6 +30,33 @@ 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
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
}
func (r *jiraRealm) ID() string {
return r.id
}
@ -44,21 +73,12 @@ 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()
if err != nil {
if err := r.ensureInited(); 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
}
@ -67,7 +87,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")
@ -78,20 +98,99 @@ func (r *jiraRealm) Register() error {
}
func (r *jiraRealm) RequestAuthSession(userID string, req json.RawMessage) interface{} {
logger := log.WithField("jira_url", r.JIRAEndpoint)
if err := r.ensureInited(); err != nil {
logger.WithError(err).Print("Failed to init realm")
return nil
}
authConfig := r.oauth1Config(r.JIRAEndpoint)
reqToken, reqSec, err := authConfig.RequestToken()
if err != nil {
logger.WithError(err).Print("Failed to request auth token")
return nil
}
logger.WithField("req_token", reqToken).Print("Received request token")
authURL, err := authConfig.AuthorizationURL(reqToken)
if err != nil {
logger.WithError(err).Print("Failed to create authorization URL")
return nil
}
_, err = database.GetServiceDB().StoreAuthSession(&JIRASession{
id: reqToken,
userID: userID,
realmID: r.id,
RequestSecret: reqSec,
})
if err != nil {
log.WithError(err).Print("Failed to store new auth session")
return nil
}
return &struct {
URL string
}{authURL.String()}
}
func (r *jiraRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request) {
logger := log.WithField("jira_url", r.JIRAEndpoint)
if err := r.ensureInited(); 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
@ -99,38 +198,39 @@ func (r *jiraRealm) jiraClient(u urls.JIRAURL, userID string, allowUnauth bool)
// make an authenticated client
var cli *jira.Client
auth := &oauth1.Config{
ConsumerKey: r.ConsumerKey,
ConsumerSecret: r.ConsumerSecret,
CallbackURL: u.Base + "realms/redirect/" + r.id,
// 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",
},
Signer: &oauth1.RSASigner{
PrivateKey: r.privateKey,
},
}
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 {
if err := r.parsePrivateKey(); 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
@ -144,6 +244,27 @@ func (r *jiraRealm) parsePrivateKey() error {
return nil
}
func (r *jiraRealm) oauth1Config(jiraBaseURL string) *oauth1.Config {
return &oauth1.Config{
ConsumerKey: r.ConsumerKey,
ConsumerSecret: r.ConsumerSecret,
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: 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,
},
}
}
func loadPrivateKey(privKeyPEM string) (*rsa.PrivateKey, error) {
// Decode PEM to grab the private key type
block, _ := pem.Decode([]byte(privKeyPEM))
@ -183,15 +304,21 @@ type jiraServiceInfo struct {
func jiraServerInfo(cli *jira.Client) (*jiraServiceInfo, error) {
var jsi jiraServiceInfo
req, _ := cli.NewRequest("GET", "rest/api/2/serverInfo", nil)
_, err := cli.Do(req, &jsi)
if err != nil {
if _, err := cli.Do(req, &jsi); err != nil {
return nil, err
}
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}
})
}

9
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

Loading…
Cancel
Save