@@ -0,0 +1,239 @@
1+package providers
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/url"
7+ "path"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
12+)
13+
14+// AtlassianProvider represents an Atlassian-based Identity Provider
15+type AtlassianProvider struct {
16+ *ProviderData
17+ Groups []string
18+}
19+
20+var _ Provider = (*AtlassianProvider)(nil)
21+
22+type me struct {
23+ AccountsType string `json:"account_type"`
24+ AccountID string `json:"account_id"`
25+ Email string `json:"email"`
26+ Name string `json:"name"`
27+ Picture string `json:"picture"`
28+ AccountStatus string `json:"account_status"`
29+ Nickname string `json:"nickname"`
30+ Locale string `json:"locale"`
31+ ExtendedProfile map[string]string `json:"extended_profile"`
32+ EmailVerified bool `json:"email_verified"`
33+}
34+
35+type resource struct {
36+ ID string `json:"id"`
37+ URL string `json:"url"`
38+ Name string `json:"name"`
39+ Scopes []string `json:"scopes"`
40+ AvatarURL string `json:"avatarUrl"`
41+}
42+
43+type resources []resource
44+
45+type group struct {
46+ Name string `json:"name"`
47+ Self string `json:"self"`
48+}
49+
50+type groups []group
51+
52+const (
53+ atlassianProviderName = "Atlassian"
54+ atlassianDefaultScope = "read:jira-user read:me"
55+ atlassianPrompt = "consent"
56+ atlassianAudience = "api.atlassian.com"
57+)
58+
59+var (
60+ // Default Login URL for Atlassian.
61+ // Pre-parsed URL of https://auth.atlassian.com/authorize.
62+ atlassianDefaultLoginURL = &url.URL{
63+ Scheme: "https",
64+ Host: "auth.atlassian.com",
65+ Path: "/authorize",
66+ }
67+
68+ // Default Redeem URL for Atlassian.
69+ // Pre-parsed URL of https://auth.atlassian.com/oauth/access_token.
70+ atlassianDefaultRedeemURL = &url.URL{
71+ Scheme: "https",
72+ Host: "auth.atlassian.com",
73+ Path: "/oauth/token",
74+ }
75+
76+ // Default Validation URL for Atlassian.
77+ // ValidationURL is the API Base URL.
78+ // Other API requests are based on this.
79+ // Pre-parsed URL of https://api.atlassian.com.
80+ atlassianDefaultValidateURL = &url.URL{
81+ Scheme: "https",
82+ Host: "api.atlassian.com",
83+ Path: "/",
84+ }
85+)
86+
87+// NewAtlassianProvider initiates a new AtlassianProvider
88+func NewAtlassianProvider(p *ProviderData) *AtlassianProvider {
89+ p.setProviderDefaults(providerDefaults{
90+ name: atlassianProviderName,
91+ loginURL: atlassianDefaultLoginURL,
92+ redeemURL: atlassianDefaultRedeemURL,
93+ profileURL: nil,
94+ validateURL: atlassianDefaultValidateURL,
95+ scope: atlassianDefaultScope,
96+ })
97+ p.Prompt = atlassianPrompt
98+ return &AtlassianProvider{ProviderData: p}
99+}
100+
101+func makeAtlassianHeader(accessToken string) http.Header {
102+ // extra headers required by the Atlassian API when making authenticated requests
103+ extraHeaders := map[string]string{
104+ acceptHeader: acceptApplicationJSON,
105+ }
106+ return makeAuthorizationHeader(tokenTypeBearer, accessToken, extraHeaders)
107+}
108+
109+// SetGroups sets Atlassian Groups
110+func (p *AtlassianProvider) SetGroups(groups []string) {
111+ p.Groups = groups
112+}
113+
114+// EnrichSessionState updates the Email and User after the initial Redeem
115+func (p *AtlassianProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
116+ err := p.validateUserGroups(ctx, s)
117+ if err != nil {
118+ return err
119+ }
120+ return nil
121+}
122+
123+// ValidateSessionState validates the AccessToken
124+func (p *AtlassianProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
125+ return validateToken(ctx, p, s.AccessToken, makeAtlassianHeader(s.AccessToken))
126+}
127+
128+// GetLoginURL overrides GetLoginURL to add Atlassian parameters
129+func (p *AtlassianProvider) GetLoginURL(redirectURI, state string) string {
130+ extraParams := url.Values{
131+ "audience": []string{atlassianAudience},
132+ }
133+ a := makeLoginURL(p.ProviderData, redirectURI, state, extraParams)
134+ return a.String()
135+}
136+
137+func (p *AtlassianProvider) validateUserGroups(ctx context.Context, s *sessions.SessionState) error {
138+ me, err := p.getSelfInfo(ctx, s)
139+ if err != nil {
140+ logger.Printf("could not get self info, reason: %s", err)
141+ return err
142+ }
143+
144+ resources, err := p.getResources(ctx, s)
145+ if err != nil {
146+ logger.Printf("could not get resources, reason: %s", err)
147+ return err
148+ }
149+
150+ isMember := false
151+ for _, r := range *resources {
152+ apiURL, err := url.Parse(r.URL)
153+ if err != nil {
154+ logger.Printf("could not parse %s, reason: %s", r.URL, err)
155+ return err
156+ }
157+
158+ groups, err := p.getGroups(ctx, s, me, apiURL)
159+ if err != nil {
160+ logger.Printf("could not get groups, reason: %s", err)
161+ return err
162+ }
163+
164+ for _, g := range *groups {
165+ for _, aug := range p.Groups {
166+ if g.Name == aug {
167+ isMember = true
168+ }
169+ }
170+ }
171+ }
172+
173+ if isMember {
174+ s.Email = me.Email
175+ s.User = me.Name
176+ }
177+
178+ return nil
179+}
180+
181+func (p *AtlassianProvider) getSelfInfo(ctx context.Context, s *sessions.SessionState) (*me, error) {
182+ var me me
183+
184+ endpoint := &url.URL{
185+ Scheme: p.ValidateURL.Scheme,
186+ Host: p.ValidateURL.Host,
187+ Path: path.Join(p.ValidateURL.Path, "/me"),
188+ }
189+ err := requests.New(endpoint.String()).
190+ WithContext(ctx).
191+ WithHeaders(makeAtlassianHeader(s.AccessToken)).
192+ Do().
193+ UnmarshalInto(&me)
194+ if err != nil {
195+ return nil, err
196+ }
197+
198+ return &me, nil
199+}
200+
201+func (p *AtlassianProvider) getResources(ctx context.Context, s *sessions.SessionState) (*resources, error) {
202+ var resources resources
203+
204+ endpoint := &url.URL{
205+ Scheme: p.ValidateURL.Scheme,
206+ Host: p.ValidateURL.Host,
207+ Path: path.Join(p.ValidateURL.Path, "/oauth/token/accessible-resources"),
208+ }
209+ err := requests.New(endpoint.String()).
210+ WithContext(ctx).
211+ WithHeaders(makeAtlassianHeader(s.AccessToken)).
212+ Do().
213+ UnmarshalInto(&resources)
214+ if err != nil {
215+ return nil, err
216+ }
217+
218+ return &resources, nil
219+}
220+
221+func (p *AtlassianProvider) getGroups(ctx context.Context, s *sessions.SessionState, me *me, apiURL *url.URL) (*groups, error) {
222+ var groups groups
223+
224+ endpoint := &url.URL{
225+ Scheme: apiURL.Scheme,
226+ Host: apiURL.Host,
227+ Path: path.Join(apiURL.Path, "/rest/api/3/user/groups"),
228+ }
229+ err := requests.New(endpoint.String() + "?accountId=" + me.AccountID).
230+ WithContext(ctx).
231+ WithHeaders(makeAtlassianHeader(s.AccessToken)).
232+ Do().
233+ UnmarshalInto(&groups)
234+ if err != nil {
235+ return nil, err
236+ }
237+
238+ return &groups, nil
239+}