Implement Steam login functionality with OpenID integration
Some checks failed
CI - Build and Push / Build and Push Docker Image (push) Failing after 15s
Some checks failed
CI - Build and Push / Build and Push Docker Image (push) Failing after 15s
This commit is contained in:
@@ -13,6 +13,28 @@ type (
|
|||||||
PingResp {
|
PingResp {
|
||||||
ok bool `json:"ok"`
|
ok bool `json:"ok"`
|
||||||
}
|
}
|
||||||
|
// Steam Login Types
|
||||||
|
SteamLoginInitReq {}
|
||||||
|
SteamLoginInitResp {
|
||||||
|
RedirectUrl string `json:"redirectUrl"`
|
||||||
|
}
|
||||||
|
SteamLoginCallbackReq {
|
||||||
|
OpenidMode string `form:"openid.mode"`
|
||||||
|
OpenidNs string `form:"openid.ns"`
|
||||||
|
OpenidOpEndpoint string `form:"openid.op_endpoint"`
|
||||||
|
OpenidClaimedId string `form:"openid.claimed_id"`
|
||||||
|
OpenidIdentity string `form:"openid.identity"`
|
||||||
|
OpenidReturnTo string `form:"openid.return_to"`
|
||||||
|
OpenidResponseNonce string `form:"openid.response_nonce"`
|
||||||
|
OpenidAssocHandle string `form:"openid.assoc_handle"`
|
||||||
|
OpenidSigned string `form:"openid.signed"`
|
||||||
|
OpenidSig string `form:"openid.sig"`
|
||||||
|
}
|
||||||
|
SteamLoginCallbackResp {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
SteamId string `json:"steamId,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@@ -25,5 +47,19 @@ service Authenticator {
|
|||||||
)
|
)
|
||||||
@handler pingHandler
|
@handler pingHandler
|
||||||
get /ping (PingReq) returns (PingResp)
|
get /ping (PingReq) returns (PingResp)
|
||||||
|
|
||||||
|
@doc (
|
||||||
|
summary: "Initiate Steam login"
|
||||||
|
description: "Redirects user to Steam OpenID login page"
|
||||||
|
)
|
||||||
|
@handler steamLoginInitHandler
|
||||||
|
get /steam/login (SteamLoginInitReq) returns (SteamLoginInitResp)
|
||||||
|
|
||||||
|
@doc (
|
||||||
|
summary: "Steam login callback"
|
||||||
|
description: "Handles the callback from Steam after user authentication"
|
||||||
|
)
|
||||||
|
@handler steamLoginCallbackHandler
|
||||||
|
get /steam/callback (SteamLoginCallbackReq) returns (SteamLoginCallbackResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
Name: Authenticator
|
Name: Authenticator
|
||||||
Host: 0.0.0.0
|
Host: 0.0.0.0
|
||||||
Port: 8888
|
Port: 8888
|
||||||
|
|
||||||
|
Steam:
|
||||||
|
CallbackURL: https://www.cialloo.com/api/authenticator/steam/callback
|
||||||
|
|||||||
@@ -4,4 +4,9 @@ import "github.com/zeromicro/go-zero/rest"
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
rest.RestConf
|
rest.RestConf
|
||||||
|
Steam SteamConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type SteamConfig struct {
|
||||||
|
CallbackURL string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/ping",
|
Path: "/ping",
|
||||||
Handler: pingHandler(serverCtx),
|
Handler: pingHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Steam login callback
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/steam/callback",
|
||||||
|
Handler: steamLoginCallbackHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Initiate Steam login
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/steam/login",
|
||||||
|
Handler: steamLoginInitHandler(serverCtx),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rest.WithPrefix("/api/authenticator"),
|
rest.WithPrefix("/api/authenticator"),
|
||||||
)
|
)
|
||||||
|
|||||||
29
src/internal/handler/steamlogincallbackhandler.go
Normal file
29
src/internal/handler/steamlogincallbackhandler.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"src/internal/logic"
|
||||||
|
"src/internal/svc"
|
||||||
|
"src/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Steam login callback
|
||||||
|
func steamLoginCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.SteamLoginCallbackReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logic.NewSteamLoginCallbackLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.SteamLoginCallback(&req)
|
||||||
|
if err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
} else {
|
||||||
|
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/internal/handler/steamlogininithandler.go
Normal file
29
src/internal/handler/steamlogininithandler.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"src/internal/logic"
|
||||||
|
"src/internal/svc"
|
||||||
|
"src/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initiate Steam login
|
||||||
|
func steamLoginInitHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.SteamLoginInitReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logic.NewSteamLoginInitLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.SteamLoginInit(&req)
|
||||||
|
if err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
} else {
|
||||||
|
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/internal/logic/steamlogincallbacklogic.go
Normal file
69
src/internal/logic/steamlogincallbacklogic.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"src/internal/svc"
|
||||||
|
"src/internal/types"
|
||||||
|
"src/internal/utils/steamauth"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SteamLoginCallbackLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Steam login callback
|
||||||
|
func NewSteamLoginCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SteamLoginCallbackLogic {
|
||||||
|
return &SteamLoginCallbackLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *SteamLoginCallbackLogic) SteamLoginCallback(req *types.SteamLoginCallbackReq) (resp *types.SteamLoginCallbackResp, err error) {
|
||||||
|
// Convert request to map for Steam validation
|
||||||
|
paramsMap := map[string]string{
|
||||||
|
"openid.mode": req.OpenidMode,
|
||||||
|
"openid.ns": req.OpenidNs,
|
||||||
|
"openid.op_endpoint": req.OpenidOpEndpoint,
|
||||||
|
"openid.claimed_id": req.OpenidClaimedId,
|
||||||
|
"openid.identity": req.OpenidIdentity,
|
||||||
|
"openid.return_to": req.OpenidReturnTo,
|
||||||
|
"openid.response_nonce": req.OpenidResponseNonce,
|
||||||
|
"openid.assoc_handle": req.OpenidAssocHandle,
|
||||||
|
"openid.signed": req.OpenidSigned,
|
||||||
|
"openid.sig": req.OpenidSig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the response with Steam
|
||||||
|
steamID, isValid, err := steamauth.ValidateResponse(paramsMap)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Errorf("Steam validation error: %v", err)
|
||||||
|
return &types.SteamLoginCallbackResp{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("Validation error: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
l.Logger.Info("Steam validation failed: invalid credentials")
|
||||||
|
return &types.SteamLoginCallbackResp{
|
||||||
|
Success: false,
|
||||||
|
Message: "Invalid Steam credentials",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Logger.Infof("Steam login successful for Steam ID: %s", steamID)
|
||||||
|
|
||||||
|
return &types.SteamLoginCallbackResp{
|
||||||
|
Success: true,
|
||||||
|
SteamId: steamID,
|
||||||
|
Message: "Login successful",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
40
src/internal/logic/steamlogininitlogic.go
Normal file
40
src/internal/logic/steamlogininitlogic.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"src/internal/svc"
|
||||||
|
"src/internal/types"
|
||||||
|
"src/internal/utils/steamauth"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SteamLoginInitLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate Steam login
|
||||||
|
func NewSteamLoginInitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SteamLoginInitLogic {
|
||||||
|
return &SteamLoginInitLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *SteamLoginInitLogic) SteamLoginInit(req *types.SteamLoginInitReq) (resp *types.SteamLoginInitResp, err error) {
|
||||||
|
// Get the callback URL from config
|
||||||
|
callbackURL := l.svcCtx.Config.Steam.CallbackURL
|
||||||
|
|
||||||
|
// Build the Steam OpenID redirect URL
|
||||||
|
redirectURL := steamauth.GetRedirectURL(callbackURL)
|
||||||
|
|
||||||
|
l.Logger.Infof("Initiating Steam login with callback URL: %s", callbackURL)
|
||||||
|
|
||||||
|
return &types.SteamLoginInitResp{
|
||||||
|
RedirectUrl: redirectURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -9,3 +9,29 @@ type PingReq struct {
|
|||||||
type PingResp struct {
|
type PingResp struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SteamLoginCallbackReq struct {
|
||||||
|
OpenidMode string `form:"openid.mode"`
|
||||||
|
OpenidNs string `form:"openid.ns"`
|
||||||
|
OpenidOpEndpoint string `form:"openid.op_endpoint"`
|
||||||
|
OpenidClaimedId string `form:"openid.claimed_id"`
|
||||||
|
OpenidIdentity string `form:"openid.identity"`
|
||||||
|
OpenidReturnTo string `form:"openid.return_to"`
|
||||||
|
OpenidResponseNonce string `form:"openid.response_nonce"`
|
||||||
|
OpenidAssocHandle string `form:"openid.assoc_handle"`
|
||||||
|
OpenidSigned string `form:"openid.signed"`
|
||||||
|
OpenidSig string `form:"openid.sig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SteamLoginCallbackResp struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
SteamId string `json:"steamId,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SteamLoginInitReq struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type SteamLoginInitResp struct {
|
||||||
|
RedirectUrl string `json:"redirectUrl"`
|
||||||
|
}
|
||||||
|
|||||||
148
src/internal/utils/steamauth/steamopenid.go
Normal file
148
src/internal/utils/steamauth/steamopenid.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package steamauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validCredRx *regexp.Regexp
|
||||||
|
var steamRx *regexp.Regexp
|
||||||
|
var provider string = "https://steamcommunity.com/openid/login"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
validCredRx = regexp.MustCompile("is_valid:true")
|
||||||
|
steamRx = regexp.MustCompile(`https://steamcommunity\.com/openid/id/(\d+)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMapToString is a utility function that aims to efficiently build a query string
|
||||||
|
// with a tiny footprint. theMap is expected to be a map of key strings with a value type of a string.
|
||||||
|
func StringMapToString(theMap map[string]string) string {
|
||||||
|
|
||||||
|
mapLength := len(theMap)
|
||||||
|
strSeparator := "&"
|
||||||
|
i := 1
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.Grow(66) // We already roughly know our base size.
|
||||||
|
|
||||||
|
for k, v := range theMap {
|
||||||
|
|
||||||
|
if i == mapLength {
|
||||||
|
strSeparator = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
|
||||||
|
fmt.Fprintf(&builder, "%s=%s%s", k, url.QueryEscape(v), strSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildQueryString is more or less building up a query string to be passed when reaching
|
||||||
|
// Steam's openid 2.0 provider (or technically any openid 2.0 provider). We only care
|
||||||
|
// that the Scheme is either http or https. Any other validation should really be done
|
||||||
|
// before using this function.
|
||||||
|
func BuildQueryString(responsePath string) string {
|
||||||
|
|
||||||
|
if responsePath[0:4] != "http" {
|
||||||
|
log.Fatal("http was not found in the responsePath!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if responsePath[4:5] != "s" {
|
||||||
|
log.Println("https isn't being used! Is this intentional?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even though the below URLs no longer function, the oauth 2.0 process formally calls
|
||||||
|
// for them and Valve actively checks for their presence.
|
||||||
|
openIdParameters := map[string]string{
|
||||||
|
"openid.mode": "checkid_setup",
|
||||||
|
"openid.return_to": responsePath,
|
||||||
|
"openid.realm": responsePath,
|
||||||
|
"openid.ns": "http://specs.openid.net/auth/2.0",
|
||||||
|
"openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
|
||||||
|
"openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringMapToString(openIdParameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateResponse is the real chunk of work that goes on. When the client comes back to our site
|
||||||
|
// we need to take what they give us in the query string and hit up the openid 2.0 provider directly
|
||||||
|
// to verify what we're being provided with is well, valid.
|
||||||
|
// If we end up with "is_valid:true" response from the Steam then isValid will always return true.
|
||||||
|
// In any other situation (credential failure, error etc) isValid will always return false.
|
||||||
|
// Takes a map[string]string to be agnostic among various http clients that exist out there
|
||||||
|
func ValidateResponse(results map[string]string) (steamID64 string, isValid bool, err error) {
|
||||||
|
|
||||||
|
openIdValidation := map[string]string{
|
||||||
|
"openid.assoc_handle": results["openid.assoc_handle"],
|
||||||
|
"openid.signed": results["openid.signed"],
|
||||||
|
"openid.sig": results["openid.sig"],
|
||||||
|
"openid.ns": results["openid.ns"],
|
||||||
|
"openid.mode": "check_authentication",
|
||||||
|
}
|
||||||
|
|
||||||
|
signedParams := strings.Split(results["openid.signed"], ",")
|
||||||
|
|
||||||
|
for _, value := range signedParams {
|
||||||
|
item := fmt.Sprintf("openid.%s", value)
|
||||||
|
if _, exists := openIdValidation[item]; !exists {
|
||||||
|
openIdValidation[item] = results[item]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urlObj, err := url.Parse(provider)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
urlObj.RawQuery = StringMapToString(openIdValidation)
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
validationResp, err := httpClient.Get(urlObj.String())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to validate %s. Error: %s ", results["openid.claimed_id"], err)
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer validationResp.Body.Close()
|
||||||
|
returnedBytes, err := ioutil.ReadAll(validationResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if validCredRx.MatchString(string(returnedBytes)) == true {
|
||||||
|
return steamRx.FindStringSubmatch(results["openid.claimed_id"])[1], true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRedirectURL builds the Steam OpenID redirect URL
|
||||||
|
func GetRedirectURL(callbackURL string) string {
|
||||||
|
urlObj, _ := url.Parse(provider)
|
||||||
|
urlObj.RawQuery = BuildQueryString(callbackURL)
|
||||||
|
return urlObj.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValuesToMap is a boilerplate function designed to convert the results of a url.Values
|
||||||
|
// in to a readable map[string]string for ValidateResponse.
|
||||||
|
// We don't get duplicate query keys supplied normally - but we'll always take the first one anyways
|
||||||
|
func ValuesToMap(fakeMap url.Values) map[string]string {
|
||||||
|
returnMap := map[string]string{}
|
||||||
|
for k, v := range fakeMap {
|
||||||
|
returnMap[k] = v[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnMap
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user