From b2d3e0665d2f9769b9e64dd5fe1ed6127d8fac89 Mon Sep 17 00:00:00 2001 From: Aeneas Rekkas Date: Wed, 27 Jan 2016 19:46:44 +0100 Subject: [PATCH] providers: added microsoft and improved existing providers --- cli/hydra-host/handler/env.go | 7 ++ oauth/handler/handler.go | 2 +- oauth/provider/dropbox/dropbox.go | 38 ++--------- oauth/provider/dropbox/dropbox_test.go | 20 ++++-- oauth/provider/google/google.go | 52 ++++++--------- oauth/provider/google/google_test.go | 49 ++++++++------ oauth/provider/microsoft/microsoft.go | 78 ++++++++++++++++++++++ oauth/provider/microsoft/microsoft_test.go | 75 +++++++++++++++++++++ 8 files changed, 233 insertions(+), 88 deletions(-) create mode 100644 oauth/provider/microsoft/microsoft.go create mode 100644 oauth/provider/microsoft/microsoft_test.go diff --git a/cli/hydra-host/handler/env.go b/cli/hydra-host/handler/env.go index e28691c7313..f50123a0768 100644 --- a/cli/hydra-host/handler/env.go +++ b/cli/hydra-host/handler/env.go @@ -7,6 +7,7 @@ import ( "github.com/ory-am/hydra/oauth/provider" "github.com/ory-am/hydra/oauth/provider/dropbox" "github.com/ory-am/hydra/oauth/provider/google" + "github.com/ory-am/hydra/oauth/provider/microsoft" "github.com/ory-am/hydra/oauth/provider/signin" "os" "path" @@ -42,6 +43,12 @@ func getEnv() { env.Getenv("GOOGLE_SECRET", ""), pkg.JoinURL(hostURL, "/oauth2/auth"), ), + microsoft.New( + "microsoft", + env.Getenv("MICROSOFT_CLIENT", ""), + env.Getenv("MICROSOFT_SECRET", ""), + pkg.JoinURL(hostURL, "/oauth2/auth"), + ), signin.New( "login", env.Getenv("SIGNIN_URL", ""), diff --git a/oauth/handler/handler.go b/oauth/handler/handler.go index d328e6f2368..678e0209fe2 100644 --- a/oauth/handler/handler.go +++ b/oauth/handler/handler.go @@ -313,7 +313,7 @@ func (h *Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request) { provider, err := h.Providers.Find(providerName) if err != nil { - http.Error(w, fmt.Sprintf(`Provider "%s" not known and no sign in location provided.`, providerName), http.StatusBadRequest) + http.Error(w, fmt.Sprintf(`Unknown provider "%s".`, providerName), http.StatusBadRequest) return } diff --git a/oauth/provider/dropbox/dropbox.go b/oauth/provider/dropbox/dropbox.go index 6fe064c0a90..05b96c8549c 100644 --- a/oauth/provider/dropbox/dropbox.go +++ b/oauth/provider/dropbox/dropbox.go @@ -16,21 +16,6 @@ type dropbox struct { api string } -type Account struct { - ID string `json:"account_id"` - Email string `json:"email"` - Locale string `json:"locale"` - ReferralURL string `json:"referral_link"` - IsPaired bool `json:"is_paired"` - Type map[string]interface{} `json:"account_type"` - Name struct { - Given string `json:"given_name,omitempty"` - Surname string `json:"surname,omitempty"` - FamiliarName string `json:"familiar_name,omitempty"` - DisplayName string `json:"display_name,omitempty"` - } `json:"name"` -} - func New(id, client, secret, redirectURL string) *dropbox { return &dropbox{ id: id, @@ -70,27 +55,18 @@ func (d *dropbox) FetchSession(code string) (Session, error) { } defer response.Body.Close() - var acc Account + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf("Could not fetch account data because %s", err) + } + + var acc map[string]interface{} if err = json.NewDecoder(response.Body).Decode(&acc); err != nil { return nil, err } return &DefaultSession{ - RemoteSubject: acc.ID, - Extra: map[string]interface{}{ - "account_id": acc.ID, - "email": acc.Email, - "locale": acc.Locale, - "referral_link": acc.ReferralURL, - "is_paired": acc.IsPaired, - "account_type": acc.Type, - "name": map[string]interface{}{ - "given_name": acc.Name.Given, - "surname": acc.Name.Surname, - "familiar_name": acc.Name.FamiliarName, - "display_name": acc.Name.DisplayName, - }, - }, + RemoteSubject: fmt.Sprintf("%s", acc["account_id"]), + Extra: acc, }, nil } diff --git a/oauth/provider/dropbox/dropbox_test.go b/oauth/provider/dropbox/dropbox_test.go index c3add9a2c3d..4c36e6c955c 100644 --- a/oauth/provider/dropbox/dropbox_test.go +++ b/oauth/provider/dropbox/dropbox_test.go @@ -48,17 +48,29 @@ func TestExchangeCode(t *testing.T) { }) router.HandleFunc("/users/get_current_account", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - fmt.Fprintln(w, `{"account_id": "dbid:2qrw3etsdtr","name": {"given_name": "Peter","surname": "Peter","familiar_name": "Peter","display_name": "Peter"},"email": "peter@gmail.com","country": "DE","locale": "de","referral_link": "https://db.tt/w34setrdgxf","is_paired": false,"account_type": {".tag": "pro"}}`) + fmt.Fprintln(w, `{ + "account_id": "dbid:foobar", + "name": { + "given_name": "foobar", + "surname": "foobar", + "familiar_name": "foobar", + "display_name": "foobar" + }, + "email": "peter@gmail.com", + "country": "DE", + "locale": "de", + "referral_link": "https://db.tt/foobar", + "is_paired": false, + "account_type": {".tag": "pro"} +}`) }) ts := httptest.NewServer(router) mock.api = ts.URL mock.conf.Endpoint.TokenURL = ts.URL + mock.conf.Endpoint.TokenURL - t.Logf("Token URL: %s", mock.conf.Endpoint.TokenURL) - t.Logf("API URL: %s", mock.api) code := "testcode" ses, err := mock.FetchSession(code) require.Nil(t, err) - assert.Equal(t, "dbid:2qrw3etsdtr", ses.GetRemoteSubject()) + assert.Equal(t, "dbid:foobar", ses.GetRemoteSubject()) } diff --git a/oauth/provider/google/google.go b/oauth/provider/google/google.go index 4639349be24..d2ffbf2fb43 100644 --- a/oauth/provider/google/google.go +++ b/oauth/provider/google/google.go @@ -16,17 +16,6 @@ type google struct { conf *oauth2.Config } -type claims struct { - Issuer string `json:"iss"` - Subject string `json:"sub"` - Email string `json:"email"` - Name string `json:"name"` - Picture string `json:"picture"` - GivenName string `json:"givenName"` - FamilyName string `json:"familyName"` - Locale string `json:"locale"` -} - func New(id, client, secret, redirectURL string) *google { return &google{ id: id, @@ -34,9 +23,14 @@ func New(id, client, secret, redirectURL string) *google { conf: &oauth2.Config{ ClientID: client, ClientSecret: secret, - Scopes: []string{"openid", "email", "profile"}, - RedirectURL: redirectURL, - Endpoint: gauth.Endpoint, + Scopes: []string{ + "email", + "profile", + "https://www.googleapis.com/auth/plus.login", + "https://www.googleapis.com/auth/plus.me", + }, + RedirectURL: redirectURL, + Endpoint: gauth.Endpoint, }, } } @@ -56,34 +50,26 @@ func (d *google) FetchSession(code string) (Session, error) { return nil, errors.Errorf("Token is not valid: %v", token) } - idToken, ok := token.Extra("id_token").(string) - if !ok { - return nil, errors.Errorf("Token is not valid: %v", idToken) - } - - resp, err := http.Get(fmt.Sprintf("%s/%s", d.api, "oauth2/v3/tokeninfo?id_token="+idToken)) + c := conf.Client(oauth2.NoContext, token) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", d.api, "plus/v1/people/me"), nil) + resp, err := c.Do(req) if err != nil { - return nil, errors.Errorf("Could not validate id token because %s", err) + return nil, err } defer resp.Body.Close() - var profile claims + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("Could not fetch account data because %s", err) + } + + var profile map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil { return nil, errors.Errorf("Could not validate id token because %s", err) } return &DefaultSession{ - RemoteSubject: profile.Subject, - Extra: map[string]interface{}{ - "iss": profile.Issuer, - "sub": profile.Subject, - "email": profile.Email, - "picture": profile.Picture, - "locale": profile.Locale, - "given_name": profile.GivenName, - "name": profile.Name, - "family_name": profile.FamilyName, - }, + RemoteSubject: fmt.Sprintf("%s", profile["id"]), + Extra: profile, }, nil } diff --git a/oauth/provider/google/google_test.go b/oauth/provider/google/google_test.go index 9201044af27..9fcb929b56c 100644 --- a/oauth/provider/google/google_test.go +++ b/oauth/provider/google/google_test.go @@ -40,29 +40,42 @@ func TestGetAuthCodeURL(t *testing.T) { } func TestExchangeCode(t *testing.T) { - router := mux.NewRouter() router.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{"access_token": "ABCDEFG", "token_type": "bearer", "uid": "12345", "id_token": "foobar"}`) }) - router.HandleFunc("/oauth2/v3/tokeninfo", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.URL.Query().Get("id_token"), "foobar") + router.HandleFunc("/plus/v1/people/me", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{ - "iss": "https://accounts.google.com", - "sub": "110169484474386276334", - "azp": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com", - "aud": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com", - "iat": "1433978353", - "exp": "1433981953", - "email": "testuser@gmail.com", - "email_verified": "true", - "name" : "Test User", - "picture": "https://lh4.googleusercontent.com/-kYgzyAWpZzJ/ABCDEFGHI/AAAJKLMNOP/tIXL9Ir44LE/s99-c/photo.jpg", - "given_name": "Test", - "family_name": "User", - "locale": "en" + "kind": "plus#person", + "etag": "\"foobar\"", + "gender": "male", + "emails": [ + { + "value": "foobar@gmail.com", + "type": "account" + } + ], + "objectType": "person", + "id": "foobarid", + "displayName": "foobar", + "name": { + "familyName": "foobar", + "givenName": "foobar" + }, + "url": "https://plus.google.com/foobar", + "image": { + "url": "https://lh3.googleusercontent.com/foobar/photo.jpg?sz=50", + "isDefault": true + }, + "isPlusUser": true, + "language": "de", + "ageRange": { + "min": 21 + }, + "circledByCount": 6, + "verified": false }`) }) ts := httptest.NewServer(router) @@ -70,10 +83,8 @@ func TestExchangeCode(t *testing.T) { mock.api = ts.URL mock.conf.Endpoint.TokenURL = ts.URL + mock.conf.Endpoint.TokenURL - t.Logf("Token URL: %s", mock.conf.Endpoint.TokenURL) - t.Logf("API URL: %s", mock.api) code := "testcode" ses, err := mock.FetchSession(code) require.Nil(t, err, "%s", err) - assert.Equal(t, "110169484474386276334", ses.GetRemoteSubject()) + assert.Equal(t, "foobarid", ses.GetRemoteSubject()) } diff --git a/oauth/provider/microsoft/microsoft.go b/oauth/provider/microsoft/microsoft.go new file mode 100644 index 00000000000..d932168fee1 --- /dev/null +++ b/oauth/provider/microsoft/microsoft.go @@ -0,0 +1,78 @@ +package microsoft + +import ( + "encoding/json" + "fmt" + "github.com/go-errors/errors" + . "github.com/ory-am/hydra/oauth/provider" + "golang.org/x/oauth2" + "net/http" +) + +// Read up on: https://dev.onedrive.com/auth/msa_oauth.htm + +type microsoft struct { + id string + conf *oauth2.Config + token *oauth2.Token + api string +} + +func New(id, client, secret, redirectURL string) *microsoft { + return µsoft{ + id: id, + api: "https://apis.live.net", + conf: &oauth2.Config{ + ClientID: client, + ClientSecret: secret, + RedirectURL: redirectURL, + Scopes: []string{"wl.signin", "wl.emails"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://login.live.com/oauth20_authorize.srf", + TokenURL: "https://login.live.com/oauth20_token.srf", + }, + }, + } +} + +func (d *microsoft) GetAuthenticationURL(state string) string { + return d.conf.AuthCodeURL(state) +} + +func (d *microsoft) FetchSession(code string) (Session, error) { + conf := *d.conf + token, err := conf.Exchange(oauth2.NoContext, code) + if err != nil { + return nil, err + } + + if !token.Valid() { + return nil, errors.Errorf("Token is not valid: %v", token) + } + + c := conf.Client(oauth2.NoContext, token) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", d.api, "v5.0/me"), nil) + response, err := c.Do(req) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf("Could not fetch account data because %s", err) + } + + var acc map[string]interface{} + if err = json.NewDecoder(response.Body).Decode(&acc); err != nil { + return nil, err + } + + return &DefaultSession{ + RemoteSubject: fmt.Sprintf("%s", acc["id"]), + Extra: acc, + }, nil +} + +func (d *microsoft) GetID() string { + return d.id +} diff --git a/oauth/provider/microsoft/microsoft_test.go b/oauth/provider/microsoft/microsoft_test.go new file mode 100644 index 00000000000..3ade66d7bf5 --- /dev/null +++ b/oauth/provider/microsoft/microsoft_test.go @@ -0,0 +1,75 @@ +package microsoft + +import ( + "fmt" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "net/http" + "net/http/httptest" + "testing" +) + +var mock = µsoft{ + id: "123", + conf: &oauth2.Config{ + ClientID: "client", + ClientSecret: "secret", + RedirectURL: "/callback", + Endpoint: oauth2.Endpoint{ + AuthURL: "/oauth2/authorize", + TokenURL: "/oauth2/token", + }, + }, +} + +func TestNew(t *testing.T) { + m := New("321", "client", "secret", "/callback") + assert.Equal(t, "321", m.id) + assert.Equal(t, "client", m.conf.ClientID) + assert.Equal(t, "secret", m.conf.ClientSecret) + assert.Equal(t, "/callback", m.conf.RedirectURL) +} +func TestGetID(t *testing.T) { + assert.Equal(t, "123", mock.GetID()) +} + +func TestGetAuthCodeURL(t *testing.T) { + require.NotEmpty(t, mock.GetAuthenticationURL("state")) +} + +func TestExchangeCode(t *testing.T) { + + router := mux.NewRouter() + router.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintln(w, `{"access_token": "ABCDEFG", "token_type": "bearer", "uid": "12345"}`) + }) + router.HandleFunc("/v5.0/me", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintln(w, `{ + "emails": { + "account": "foo@bar.com", + "business": "", + "personal": "", + "preferred": "foo@bar.com" + }, + "first_name": "Foo", + "gender": "", + "id": "foobarid", + "last_name": "Bar", + "locale": "en_US", + "name": "Foo Bar" +}`) + }) + ts := httptest.NewServer(router) + + mock.api = ts.URL + mock.conf.Endpoint.TokenURL = ts.URL + mock.conf.Endpoint.TokenURL + + code := "testcode" + ses, err := mock.FetchSession(code) + require.Nil(t, err) + assert.Equal(t, "foobarid", ses.GetRemoteSubject()) +}