git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #84 from balshor/master

Add LinkedIn provider

commit26170c56af0244785df54713e6673cb74a1c7bce authorJehiah Czebotar <jehiah@gmail.com> dateFri 17 Apr 2015, 23:40 (12 years ago) parent78e080e5bc77b0
4 files changed+210−2
README.md+12−2 providers/linkedin.go+68 providers/linkedin_test.go+128 providers/providers.go+2
README.md+12−2
@@ -33,8 +33,9 @@ providers to validate individual accounts, or a whole google apps domain.
3333
3434You will need to register an OAuth application with Google (or [another
3535provider](#providers)), and configure it with Redirect URI(s) for the domain
36−you intend to run `google_auth_proxy` on. For Google, the registration steps
37−are:
36+you intend to run `google_auth_proxy` on.
37+
38+For Google, the registration steps are:
3839
39401. Create a new project: https://console.developers.google.com/project
40412. Under "APIs & Auth", choose "Credentials"
@@ -47,6 +48,14 @@ are:
4748 * Fill in the necessary fields and Save (this is _required_)
48495. Take note of the **Client ID** and **Client Secret**
4950
51+For LinkedIn, the registration steps are:
52+
53+1. Create a new project: https://www.linkedin.com/secure/developer
54+2. In the OAuth User Agreement section:
55+ * In default scope, select r_basicprofile and r_emailaddress.
56+ * In "OAuth 2.0 Redirect URLs", enter `https://internal.yourcompany.com/oauth2/callback`
57+3. Fill in the remaining required fields and Save.
58+4. Take note of the **Consumer Key / API Key** and **Consumer Secret / Secret Key**
5059
5160## Configuration
5261
@@ -160,6 +169,7 @@ directive. Right now this includes:
160169
161170* `myusa` - The [MyUSA](https://alpha.my.usa.gov) authentication service
162171 ([GitHub](https://github.com/18F/myusa))
172+* `linkedin` - The [LinkedIn](https://developer.linkedin.com/docs/signin-with-linkedin) Sign In service.
163173
164174## Adding a new Provider
165175
providers/linkedin.gonew file+68
@@ -0,0 +1,68 @@
1+package providers
2+
3+import (
4+ "bytes"
5+ "errors"
6+ "fmt"
7+ "log"
8+ "net/http"
9+ "net/url"
10+
11+ "github.com/bitly/go-simplejson"
12+ "github.com/bitly/google_auth_proxy/api"
13+)
14+
15+type LinkedInProvider struct {
16+ *ProviderData
17+}
18+
19+func NewLinkedInProvider(p *ProviderData) *LinkedInProvider {
20+ p.ProviderName = "LinkedIn"
21+ if p.LoginUrl.String() == "" {
22+ p.LoginUrl = &url.URL{Scheme: "https",
23+ Host: "www.linkedin.com",
24+ Path: "/uas/oauth2/authorization"}
25+ }
26+ if p.RedeemUrl.String() == "" {
27+ p.RedeemUrl = &url.URL{Scheme: "https",
28+ Host: "www.linkedin.com",
29+ Path: "/uas/oauth2/accessToken"}
30+ }
31+ if p.ProfileUrl.String() == "" {
32+ p.ProfileUrl = &url.URL{Scheme: "https",
33+ Host: "www.linkedin.com",
34+ Path: "/v1/people/~/email-address"}
35+ }
36+ if p.Scope == "" {
37+ p.Scope = "r_emailaddress r_basicprofile"
38+ }
39+ return &LinkedInProvider{ProviderData: p}
40+}
41+
42+func (p *LinkedInProvider) GetEmailAddress(unused_auth_response *simplejson.Json,
43+ access_token string) (string, error) {
44+ if access_token == "" {
45+ return "", errors.New("missing access token")
46+ }
47+ params := url.Values{}
48+ req, err := http.NewRequest("GET", p.ProfileUrl.String()+"?format=json", bytes.NewBufferString(params.Encode()))
49+ if err != nil {
50+ return "", err
51+ }
52+ req.Header.Set("Accept", "application/json")
53+ req.Header.Set("x-li-format", "json")
54+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
55+
56+ json, err := api.Request(req)
57+ if err != nil {
58+ log.Printf("failed making request %s", err)
59+ return "", err
60+ }
61+
62+ email, err := json.String()
63+ if err != nil {
64+ log.Printf("failed making request %s", err)
65+ return "", err
66+ }
67+ return email, nil
68+}
providers/linkedin_test.gonew file+128
@@ -0,0 +1,128 @@
1+package providers
2+
3+import (
4+ "github.com/bitly/go-simplejson"
5+ "github.com/bmizerany/assert"
6+ "net/http"
7+ "net/http/httptest"
8+ "net/url"
9+ "testing"
10+)
11+
12+func testLinkedInProvider(hostname string) *LinkedInProvider {
13+ p := NewLinkedInProvider(
14+ &ProviderData{
15+ ProviderName: "",
16+ LoginUrl: &url.URL{},
17+ RedeemUrl: &url.URL{},
18+ ProfileUrl: &url.URL{},
19+ Scope: ""})
20+ if hostname != "" {
21+ updateUrl(p.Data().LoginUrl, hostname)
22+ updateUrl(p.Data().RedeemUrl, hostname)
23+ updateUrl(p.Data().ProfileUrl, hostname)
24+ }
25+ return p
26+}
27+
28+func testLinkedInBackend(payload string) *httptest.Server {
29+ path := "/v1/people/~/email-address"
30+
31+ return httptest.NewServer(http.HandlerFunc(
32+ func(w http.ResponseWriter, r *http.Request) {
33+ url := r.URL
34+ if url.Path != path {
35+ w.WriteHeader(404)
36+ } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
37+ w.WriteHeader(403)
38+ } else {
39+ w.WriteHeader(200)
40+ w.Write([]byte(payload))
41+ }
42+ }))
43+}
44+
45+func TestLinkedInProviderDefaults(t *testing.T) {
46+ p := testLinkedInProvider("")
47+ assert.NotEqual(t, nil, p)
48+ assert.Equal(t, "LinkedIn", p.Data().ProviderName)
49+ assert.Equal(t, "https://www.linkedin.com/uas/oauth2/authorization",
50+ p.Data().LoginUrl.String())
51+ assert.Equal(t, "https://www.linkedin.com/uas/oauth2/accessToken",
52+ p.Data().RedeemUrl.String())
53+ assert.Equal(t, "https://www.linkedin.com/v1/people/~/email-address",
54+ p.Data().ProfileUrl.String())
55+ assert.Equal(t, "r_emailaddress r_basicprofile", p.Data().Scope)
56+}
57+
58+func TestLinkedInProviderOverrides(t *testing.T) {
59+ p := NewLinkedInProvider(
60+ &ProviderData{
61+ LoginUrl: &url.URL{
62+ Scheme: "https",
63+ Host: "example.com",
64+ Path: "/oauth/auth"},
65+ RedeemUrl: &url.URL{
66+ Scheme: "https",
67+ Host: "example.com",
68+ Path: "/oauth/token"},
69+ ProfileUrl: &url.URL{
70+ Scheme: "https",
71+ Host: "example.com",
72+ Path: "/oauth/profile"},
73+ Scope: "profile"})
74+ assert.NotEqual(t, nil, p)
75+ assert.Equal(t, "LinkedIn", p.Data().ProviderName)
76+ assert.Equal(t, "https://example.com/oauth/auth",
77+ p.Data().LoginUrl.String())
78+ assert.Equal(t, "https://example.com/oauth/token",
79+ p.Data().RedeemUrl.String())
80+ assert.Equal(t, "https://example.com/oauth/profile",
81+ p.Data().ProfileUrl.String())
82+ assert.Equal(t, "profile", p.Data().Scope)
83+}
84+
85+func TestLinkedInProviderGetEmailAddress(t *testing.T) {
86+ b := testLinkedInBackend(`"user@linkedin.com"`)
87+ defer b.Close()
88+
89+ b_url, _ := url.Parse(b.URL)
90+ p := testLinkedInProvider(b_url.Host)
91+ unused_auth_response := simplejson.New()
92+
93+ email, err := p.GetEmailAddress(unused_auth_response,
94+ "imaginary_access_token")
95+ assert.Equal(t, nil, err)
96+ assert.Equal(t, "user@linkedin.com", email)
97+}
98+
99+func TestLinkedInProviderGetEmailAddressFailedRequest(t *testing.T) {
100+ b := testLinkedInBackend("unused payload")
101+ defer b.Close()
102+
103+ b_url, _ := url.Parse(b.URL)
104+ p := testLinkedInProvider(b_url.Host)
105+ unused_auth_response := simplejson.New()
106+
107+ // We'll trigger a request failure by using an unexpected access
108+ // token. Alternatively, we could allow the parsing of the payload as
109+ // JSON to fail.
110+ email, err := p.GetEmailAddress(unused_auth_response,
111+ "unexpected_access_token")
112+ assert.NotEqual(t, nil, err)
113+ assert.Equal(t, "", email)
114+}
115+
116+func TestLinkedInProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
117+ b := testLinkedInBackend("{\"foo\": \"bar\"}")
118+ defer b.Close()
119+
120+ b_url, _ := url.Parse(b.URL)
121+ p := testLinkedInProvider(b_url.Host)
122+ unused_auth_response := simplejson.New()
123+
124+ email, err := p.GetEmailAddress(unused_auth_response,
125+ "imaginary_access_token")
126+ assert.NotEqual(t, nil, err)
127+ assert.Equal(t, "", email)
128+}
providers/providers.go+2
@@ -14,6 +14,8 @@ func New(provider string, p *ProviderData) Provider {
1414 switch provider {
1515 case "myusa":
1616 return NewMyUsaProvider(p)
17+ case "linkedin":
18+ return NewLinkedInProvider(p)
1719 default:
1820 return NewGoogleProvider(p)
1921 }