Skip to content

Commit 6af3846

Browse files
authored
Merge pull request #328 from kenkoii/kakao-login-provider
Added Kakao Talk provider
2 parents fa439f7 + 111dc77 commit 6af3846

File tree

6 files changed

+333
-0
lines changed

6 files changed

+333
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ $ go get github.com/markbates/goth
4141
* InfluxCloud
4242
* Instagram
4343
* Intercom
44+
* Kakao
4445
* Lastfm
4546
* Linkedin
4647
* LINE

examples/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/markbates/goth/providers/heroku"
3737
"github.com/markbates/goth/providers/instagram"
3838
"github.com/markbates/goth/providers/intercom"
39+
"github.com/markbates/goth/providers/kakao"
3940
"github.com/markbates/goth/providers/lastfm"
4041
"github.com/markbates/goth/providers/line"
4142
"github.com/markbates/goth/providers/linkedin"
@@ -98,6 +99,7 @@ func main() {
9899
microsoftonline.New(os.Getenv("MICROSOFTONLINE_KEY"), os.Getenv("MICROSOFTONLINE_SECRET"), "http://localhost:3000/auth/microsoftonline/callback"),
99100
battlenet.New(os.Getenv("BATTLENET_KEY"), os.Getenv("BATTLENET_SECRET"), "http://localhost:3000/auth/battlenet/callback"),
100101
eveonline.New(os.Getenv("EVEONLINE_KEY"), os.Getenv("EVEONLINE_SECRET"), "http://localhost:3000/auth/eveonline/callback"),
102+
kakao.New(os.Getenv("KAKAO_KEY"), os.Getenv("KAKAO_SECRET"), "http://localhost:3000/auth/kakao/callback"),
101103

102104
//Pointed localhost.com to http://localhost:3000/auth/yahoo/callback through proxy as yahoo
103105
// does not allow to put custom ports in redirection uri
@@ -170,6 +172,7 @@ func main() {
170172
m["heroku"] = "Heroku"
171173
m["instagram"] = "Instagram"
172174
m["intercom"] = "Intercom"
175+
m["kakao"] = "Kakao"
173176
m["lastfm"] = "Last FM"
174177
m["linkedin"] = "Linkedin"
175178
m["line"] = "LINE"

providers/kakao/kakao.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Package kakao implements the OAuth2 protocol for authenticating users through kakao.
2+
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
3+
package kakao
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"io/ioutil"
9+
"net/http"
10+
"strconv"
11+
12+
"fmt"
13+
14+
"github.com/markbates/goth"
15+
"golang.org/x/oauth2"
16+
)
17+
18+
const (
19+
authURL string = "https://kauth.kakao.com/oauth/authorize"
20+
tokenURL string = "https://kauth.kakao.com/oauth/token"
21+
endpointUser string = "https://kapi.kakao.com/v2/user/me"
22+
)
23+
24+
// Provider is the implementation of `goth.Provider` for accessing Kakao.
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 Kakao provider and sets up important connection details.
35+
// You should always call `kakao.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: "kakao",
43+
}
44+
p.config = newConfig(p, scopes)
45+
return p
46+
}
47+
48+
// Name is the name used to retrieve this provider later.
49+
func (p *Provider) Name() string {
50+
return p.providerName
51+
}
52+
53+
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
54+
func (p *Provider) SetName(name string) {
55+
p.providerName = name
56+
}
57+
58+
// Client returns a pointer to http.Client setting some client fallback.
59+
func (p *Provider) Client() *http.Client {
60+
return goth.HTTPClientWithFallBack(p.HTTPClient)
61+
}
62+
63+
// Debug is a no-op for the kakao package.
64+
func (p *Provider) Debug(debug bool) {}
65+
66+
// BeginAuth asks kakao for an authentication end-point.
67+
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
68+
return &Session{
69+
AuthURL: p.config.AuthCodeURL(state),
70+
}, nil
71+
}
72+
73+
// FetchUser will go to kakao and access basic information about the user.
74+
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
75+
sess := session.(*Session)
76+
user := goth.User{
77+
AccessToken: sess.AccessToken,
78+
Provider: p.Name(),
79+
RefreshToken: sess.RefreshToken,
80+
ExpiresAt: sess.ExpiresAt,
81+
}
82+
83+
if user.AccessToken == "" {
84+
// data is not yet retrieved since accessToken is still empty
85+
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
86+
}
87+
88+
// Get the userID, kakao needs userID in order to get user profile info
89+
c := p.Client()
90+
req, err := http.NewRequest("GET", endpointUser, nil)
91+
if err != nil {
92+
return user, err
93+
}
94+
95+
req.Header.Add("Authorization", "Bearer "+sess.AccessToken)
96+
97+
response, err := c.Do(req)
98+
if err != nil {
99+
if response != nil {
100+
response.Body.Close()
101+
}
102+
return user, err
103+
}
104+
105+
if response.StatusCode != http.StatusOK {
106+
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
107+
}
108+
109+
bits, err := ioutil.ReadAll(response.Body)
110+
if err != nil {
111+
return user, err
112+
}
113+
114+
u := struct {
115+
ID int `json:"id"`
116+
Properties struct {
117+
Nickname string `json:"nickname"`
118+
ThumbnailImage string `json:"thumbnail_image"`
119+
ProfileImage string `json:"profile_image"`
120+
} `json:"properties"`
121+
}{}
122+
123+
if err = json.NewDecoder(bytes.NewReader(bits)).Decode(&u); err != nil {
124+
return user, err
125+
}
126+
127+
id := strconv.Itoa(u.ID)
128+
129+
user.NickName = u.Properties.Nickname
130+
user.AvatarURL = u.Properties.ProfileImage
131+
user.UserID = id
132+
return user, err
133+
}
134+
135+
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
136+
c := &oauth2.Config{
137+
ClientID: provider.ClientKey,
138+
ClientSecret: provider.Secret,
139+
RedirectURL: provider.CallbackURL,
140+
Endpoint: oauth2.Endpoint{
141+
AuthURL: authURL,
142+
TokenURL: tokenURL,
143+
},
144+
Scopes: []string{},
145+
}
146+
147+
if len(scopes) > 0 {
148+
for _, scope := range scopes {
149+
c.Scopes = append(c.Scopes, scope)
150+
}
151+
}
152+
return c
153+
}
154+
155+
//RefreshTokenAvailable refresh token is provided by auth provider or not
156+
func (p *Provider) RefreshTokenAvailable() bool {
157+
return false
158+
}
159+
160+
//RefreshToken get new access token based on the refresh token
161+
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
162+
return nil, nil
163+
}

providers/kakao/kakao_test.go

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

providers/kakao/session.go

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

providers/kakao/session_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package kakao_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/markbates/goth"
7+
"github.com/markbates/goth/providers/line"
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 := &line.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 := &line.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 := &line.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 := &line.Session{}
46+
47+
a.Equal(s.String(), s.Marshal())
48+
}

0 commit comments

Comments
 (0)