1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
|
// Package steam implements the OpenID protocol for authenticating users through Steam.
package steam
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
const (
// Steam API Endpoints
apiLoginEndpoint = "https://steamcommunity.com/openid/login"
apiUserSummaryEndpoint = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s"
// OpenID settings
openIDMode = "checkid_setup"
openIDNs = "http://specs.openid.net/auth/2.0"
openIDIdentifier = "http://specs.openid.net/auth/2.0/identifier_select"
)
// New creates a new Steam provider, and sets up important connection details.
// You should always call `steam.New` to get a new Provider. Never try to create
// one manually.
func New(apiKey string, callbackURL string) *Provider {
p := &Provider{
APIKey: apiKey,
CallbackURL: callbackURL,
providerName: "steam",
}
return p
}
// Provider is the implementation of `goth.Provider` for accessing Steam
type Provider struct {
APIKey string
CallbackURL string
HTTPClient *http.Client
providerName string
}
// Name gets the name used to retrieve this provider.
func (p *Provider) Name() string {
return p.providerName
}
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
func (p *Provider) SetName(name string) {
p.providerName = name
}
func (p *Provider) Client() *http.Client {
return goth.HTTPClientWithFallBack(p.HTTPClient)
}
// Debug is no-op for the Steam package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth will return the authentication end-point for Steam.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
u, err := p.getAuthURL()
s := &Session{
AuthURL: u.String(),
CallbackURL: p.CallbackURL,
}
return s, err
}
// getAuthURL is an internal function to build the correct
// authentication url to redirect the user to Steam.
func (p *Provider) getAuthURL() (*url.URL, error) {
callbackURL, err := url.Parse(p.CallbackURL)
if err != nil {
return nil, err
}
urlValues := map[string]string{
"openid.claimed_id": openIDIdentifier,
"openid.identity": openIDIdentifier,
"openid.mode": openIDMode,
"openid.ns": openIDNs,
"openid.realm": fmt.Sprintf("%s://%s", callbackURL.Scheme, callbackURL.Host),
"openid.return_to": callbackURL.String(),
}
u, err := url.Parse(apiLoginEndpoint)
if err != nil {
return nil, err
}
v := u.Query()
for key, value := range urlValues {
v.Set(key, value)
}
u.RawQuery = v.Encode()
return u, nil
}
// FetchUser will go to Steam and access basic info about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
s := session.(*Session)
u := goth.User{
Provider: p.Name(),
AccessToken: s.ResponseNonce,
}
if s.SteamID == "" {
// data is not yet retrieved since SteamID is still empty
return u, fmt.Errorf("%s cannot get user information without SteamID", p.providerName)
}
apiURL := fmt.Sprintf(apiUserSummaryEndpoint, p.APIKey, s.SteamID)
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return u, err
}
req.Header.Add("Accept", "application/json")
resp, err := p.Client().Do(req)
if err != nil {
if resp != nil {
resp.Body.Close()
}
return u, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return u, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
}
u, err = buildUserObject(resp.Body, u)
return u, err
}
// buildUserObject is an internal function to build a goth.User object
// based in the data stored in r
func buildUserObject(r io.Reader, u goth.User) (goth.User, error) {
// Response object from Steam
apiResponse := struct {
Response struct {
Players []struct {
UserID string `json:"steamid"`
NickName string `json:"personaname"`
Name string `json:"realname"`
AvatarURL string `json:"avatarfull"`
LocationCountryCode string `json:"loccountrycode"`
LocationStateCode string `json:"locstatecode"`
} `json:"players"`
} `json:"response"`
}{}
err := json.NewDecoder(r).Decode(&apiResponse)
if err != nil {
return u, err
}
if l := len(apiResponse.Response.Players); l != 1 {
return u, fmt.Errorf("Expected one player in API response. Got %d.", l)
}
player := apiResponse.Response.Players[0]
u.UserID = player.UserID
u.Name = player.Name
if len(player.Name) == 0 {
u.Name = "No name is provided by the Steam API"
}
u.NickName = player.NickName
u.AvatarURL = player.AvatarURL
u.Email = "No email is provided by the Steam API"
u.Description = "No description is provided by the Steam API"
if len(player.LocationStateCode) > 0 && len(player.LocationCountryCode) > 0 {
u.Location = fmt.Sprintf("%s, %s", player.LocationStateCode, player.LocationCountryCode)
} else if len(player.LocationCountryCode) > 0 {
u.Location = player.LocationCountryCode
} else if len(player.LocationStateCode) > 0 {
u.Location = player.LocationStateCode
} else {
u.Location = "No location is provided by the Steam API"
}
return u, nil
}
// RefreshToken refresh token is not provided by Steam
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
return nil, nil
}
// RefreshTokenAvailable refresh token is not provided by Steam
func (p *Provider) RefreshTokenAvailable() bool {
return false
}
|