Skip to content

Commit db35608

Browse files
committed
Added Atlassian Provider
1 parent db5b76a commit db35608

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ $ go get github.com/markbates/goth
1818

1919
* Amazon
2020
* Apple
21+
* Atlassian
2122
* Auth0
2223
* Azure AD
2324
* Battle.net

examples/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/markbates/goth/gothic"
1616
"github.com/markbates/goth/providers/amazon"
1717
"github.com/markbates/goth/providers/apple"
18+
"github.com/markbates/goth/providers/atlassian"
1819
"github.com/markbates/goth/providers/auth0"
1920
"github.com/markbates/goth/providers/azuread"
2021
"github.com/markbates/goth/providers/battlenet"
@@ -132,6 +133,7 @@ func main() {
132133
shopify.New(os.Getenv("SHOPIFY_KEY"), os.Getenv("SHOPIFY_SECRET"), "http://localhost:3000/auth/shopify/callback", shopify.ScopeReadCustomers, shopify.ScopeReadOrders),
133134
apple.New(os.Getenv("APPLE_KEY"), os.Getenv("APPLE_SECRET"), "http://localhost:3000/auth/apple/callback", nil, apple.ScopeName, apple.ScopeEmail),
134135
strava.New(os.Getenv("STRAVA_KEY"), os.Getenv("STRAVA_SECRET"), "http://localhost:3000/auth/strava/callback"),
136+
atlassian.New(os.Getenv("ATLASSIAN_KEY"), os.Getenv("ATLASSIAN_SECRET"), "http://localhost:3000/auth/atlassian/callback"),
135137
)
136138

137139
// OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html)
@@ -196,6 +198,7 @@ func main() {
196198
m["seatalk"] = "SeaTalk"
197199
m["apple"] = "Apple"
198200
m["strava"] = "Strava"
201+
m["atlassian"] = "Atlassian"
199202

200203
var keys []string
201204
for k := range m {

providers/atlassian/atlassian.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Package atlassian implements the OAuth2 protocol for authenticating users through atlassian.
2+
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
3+
package atlassian
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"io"
9+
"io/ioutil"
10+
"net/http"
11+
12+
"fmt"
13+
14+
"github.com/markbates/goth"
15+
"golang.org/x/oauth2"
16+
)
17+
18+
const (
19+
authURL string = "https://auth.atlassian.com/authorize"
20+
tokenURL string = "https://auth.atlassian.com/oauth/token"
21+
endpointProfile string = "https://api.atlassian.com/me"
22+
)
23+
24+
// Provider is the implementation of `goth.Provider` for accessing Atlassian.
25+
type Provider struct {
26+
ClientKey string
27+
Secret string
28+
CallbackURL string
29+
HTTPClient *http.Client
30+
config *oauth2.Config
31+
providerName string
32+
}
33+
34+
// New creates a new Atlassian provider and sets up important connection details.
35+
// You should always call `atlassian.New` to get a new provider. Never try to
36+
// create one manually.
37+
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
38+
p := &Provider{
39+
ClientKey: clientKey,
40+
Secret: secret,
41+
CallbackURL: callbackURL,
42+
providerName: "atlassian",
43+
}
44+
p.config = newConfig(p, scopes)
45+
return p
46+
}
47+
48+
func (p *Provider) Client() *http.Client {
49+
return goth.HTTPClientWithFallBack(p.HTTPClient)
50+
}
51+
52+
// Name is the name used to retrieve this provider later.
53+
func (p *Provider) Name() string {
54+
return p.providerName
55+
}
56+
57+
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
58+
func (p *Provider) SetName(name string) {
59+
p.providerName = name
60+
}
61+
62+
// Debug is a no-op for the atlassian package.
63+
func (p *Provider) Debug(debug bool) {}
64+
65+
// BeginAuth asks Atlassian for an authentication end-point.
66+
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
67+
authUrl := p.config.AuthCodeURL(state)
68+
// audience and prompt are required static fields as described by
69+
// https://developer.atlassian.com/cloud/atlassian/platform/oauth-2-authorization-code-grants-3lo-for-apps/#authcode
70+
authUrl += "&audience=api.atlassian.com&prompt=consent"
71+
return &Session{
72+
AuthURL: authUrl,
73+
}, nil
74+
}
75+
76+
// FetchUser will go to Atlassian and access basic information about the user.
77+
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
78+
s := session.(*Session)
79+
user := goth.User{
80+
AccessToken: s.AccessToken,
81+
Provider: p.Name(),
82+
RefreshToken: s.RefreshToken,
83+
ExpiresAt: s.ExpiresAt,
84+
}
85+
86+
if user.AccessToken == "" {
87+
// data is not yet retrieved since accessToken is still empty
88+
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
89+
}
90+
91+
c := p.Client()
92+
req, err := http.NewRequest("GET", endpointProfile, nil)
93+
if err != nil {
94+
return user, err
95+
}
96+
req.Header.Set("Authorization", "Bearer "+s.AccessToken)
97+
response, err := c.Do(req)
98+
99+
if err != nil {
100+
return user, err
101+
}
102+
defer response.Body.Close()
103+
104+
if response.StatusCode != http.StatusOK {
105+
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
106+
}
107+
108+
bits, err := ioutil.ReadAll(response.Body)
109+
if err != nil {
110+
return user, err
111+
}
112+
113+
err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
114+
if err != nil {
115+
return user, err
116+
}
117+
118+
err = userFromReader(bytes.NewReader(bits), &user)
119+
return user, err
120+
}
121+
122+
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
123+
c := &oauth2.Config{
124+
ClientID: provider.ClientKey,
125+
ClientSecret: provider.Secret,
126+
RedirectURL: provider.CallbackURL,
127+
Endpoint: oauth2.Endpoint{
128+
AuthURL: authURL,
129+
TokenURL: tokenURL,
130+
},
131+
Scopes: []string{},
132+
}
133+
if len(scopes) > 0 {
134+
for _, scope := range scopes {
135+
c.Scopes = append(c.Scopes, scope)
136+
}
137+
} else {
138+
c.Scopes = append(c.Scopes, "read:me")
139+
}
140+
return c
141+
}
142+
143+
func userFromReader(r io.Reader, user *goth.User) error {
144+
145+
u := struct {
146+
Name string `json:"name"`
147+
NickName string `json:"nickname"`
148+
ExtendedProfile struct {
149+
Location string `json:"location"`
150+
} `json:"extended_profile"`
151+
Email string `json:"email"`
152+
ID string `json:"account_id"`
153+
AvatarURL string `json:"picture"`
154+
}{}
155+
err := json.NewDecoder(r).Decode(&u)
156+
if err != nil {
157+
return err
158+
}
159+
user.Email = u.Email
160+
user.Name = u.Name
161+
user.NickName = u.NickName
162+
user.UserID = u.ID
163+
user.Location = u.ExtendedProfile.Location
164+
user.AvatarURL = u.AvatarURL
165+
166+
return err
167+
}
168+
169+
//RefreshTokenAvailable refresh token is provided by auth provider or not
170+
func (p *Provider) RefreshTokenAvailable() bool {
171+
return true
172+
}
173+
174+
//RefreshToken get new access token based on the refresh token
175+
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
176+
token := &oauth2.Token{RefreshToken: refreshToken}
177+
ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token)
178+
newToken, err := ts.Token()
179+
if err != nil {
180+
return nil, err
181+
}
182+
return newToken, err
183+
}

providers/atlassian/atlassian_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package atlassian_test
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"testing"
7+
8+
"github.com/markbates/goth"
9+
"github.com/markbates/goth/providers/atlassian"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_New(t *testing.T) {
14+
t.Parallel()
15+
a := assert.New(t)
16+
p := provider()
17+
18+
a.Equal(p.ClientKey, os.Getenv("JIRA_KEY"))
19+
a.Equal(p.Secret, os.Getenv("JIRA_SECRET"))
20+
a.Equal(p.CallbackURL, "/foo")
21+
}
22+
23+
func Test_Implements_Provider(t *testing.T) {
24+
t.Parallel()
25+
a := assert.New(t)
26+
a.Implements((*goth.Provider)(nil), provider())
27+
}
28+
29+
func Test_BeginAuth(t *testing.T) {
30+
t.Parallel()
31+
a := assert.New(t)
32+
p := provider()
33+
session, err := p.BeginAuth("test_state")
34+
s := session.(*atlassian.Session)
35+
a.NoError(err)
36+
a.Contains(s.AuthURL, "https://auth.atlassian.com/authorize")
37+
a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", os.Getenv("JIRA_KEY")))
38+
a.Contains(s.AuthURL, "state=test_state")
39+
a.Contains(s.AuthURL, "scope=read%3Ame")
40+
}
41+
42+
func Test_SessionFromJSON(t *testing.T) {
43+
t.Parallel()
44+
a := assert.New(t)
45+
46+
p := provider()
47+
session, err := p.UnmarshalSession(`{"AuthURL":"https://auth.atlassian.com/auth_url","AccessToken":"1234567890"}`)
48+
a.NoError(err)
49+
50+
s := session.(*atlassian.Session)
51+
a.Equal(s.AuthURL, "https://auth.atlassian.com/auth_url")
52+
a.Equal(s.AccessToken, "1234567890")
53+
}
54+
55+
func provider() *atlassian.Provider {
56+
return atlassian.New(os.Getenv("JIRA_KEY"), os.Getenv("JIRA_SECRET"), "/foo")
57+
}

providers/atlassian/session.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package atlassian
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"strings"
7+
"time"
8+
9+
"github.com/markbates/goth"
10+
)
11+
12+
// Session stores data during the auth process with Atlassian.
13+
type Session struct {
14+
AuthURL string
15+
AccessToken string
16+
RefreshToken string
17+
ExpiresAt time.Time
18+
}
19+
20+
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Atlassian provider.
21+
func (s Session) GetAuthURL() (string, error) {
22+
if s.AuthURL == "" {
23+
return "", errors.New(goth.NoAuthUrlErrorMessage)
24+
}
25+
return s.AuthURL, nil
26+
}
27+
28+
// Authorize the session with Atlassian and return the access token to be stored for future use.
29+
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
30+
p := provider.(*Provider)
31+
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"))
32+
if err != nil {
33+
return "", err
34+
}
35+
36+
if !token.Valid() {
37+
return "", errors.New("Invalid token received from provider")
38+
}
39+
40+
s.AccessToken = token.AccessToken
41+
s.RefreshToken = token.RefreshToken
42+
s.ExpiresAt = token.Expiry
43+
return token.AccessToken, err
44+
}
45+
46+
// Marshal the session into a string
47+
func (s Session) Marshal() string {
48+
b, _ := json.Marshal(s)
49+
return string(b)
50+
}
51+
52+
func (s Session) String() string {
53+
return s.Marshal()
54+
}
55+
56+
// UnmarshalSession will unmarshal a JSON string into a session.
57+
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
58+
s := &Session{}
59+
err := json.NewDecoder(strings.NewReader(data)).Decode(s)
60+
return s, err
61+
}

providers/atlassian/session_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package atlassian_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/markbates/goth"
7+
"github.com/markbates/goth/providers/atlassian"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func Test_Implements_Session(t *testing.T) {
12+
t.Parallel()
13+
a := assert.New(t)
14+
s := &atlassian.Session{}
15+
16+
a.Implements((*goth.Session)(nil), s)
17+
}
18+
19+
func Test_GetAuthURL(t *testing.T) {
20+
t.Parallel()
21+
a := assert.New(t)
22+
s := &atlassian.Session{}
23+
24+
_, err := s.GetAuthURL()
25+
a.Error(err)
26+
27+
s.AuthURL = "/foo"
28+
29+
url, _ := s.GetAuthURL()
30+
a.Equal(url, "/foo")
31+
}
32+
33+
func Test_ToJSON(t *testing.T) {
34+
t.Parallel()
35+
a := assert.New(t)
36+
s := &atlassian.Session{}
37+
38+
data := s.Marshal()
39+
a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`)
40+
}
41+
42+
func Test_String(t *testing.T) {
43+
t.Parallel()
44+
a := assert.New(t)
45+
s := &atlassian.Session{}
46+
47+
a.Equal(s.String(), s.Marshal())
48+
}

0 commit comments

Comments
 (0)