Initial commit
This commit is contained in:
commit
4c69ddb99f
35
.editorconfig
Normal file
35
.editorconfig
Normal file
@ -0,0 +1,35 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
|
||||
end_of_line = LF
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.sh]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[{*.html,*.js,*.css,*.scss}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[{{*.,}[Dd]ockerfile{.*,},{*.,}[Cc]ontainerfile{.*,}}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.proto]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{*.go,go.mod}]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
25
LICENSE
Normal file
25
LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2024, some
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
48
client/authorization.go
Normal file
48
client/authorization.go
Normal file
@ -0,0 +1,48 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"somehole.com/common/oauth2/session"
|
||||
)
|
||||
|
||||
type AuthorizationUrl struct {
|
||||
*Client
|
||||
*session.Session
|
||||
ResponseType string
|
||||
}
|
||||
|
||||
func NewAuthorizationUrl(client *Client, id session.SessionId, responseType string) (url *AuthorizationUrl, err error) {
|
||||
ses, ok := client.sessions[id]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no session found")
|
||||
}
|
||||
url = &AuthorizationUrl{
|
||||
Client: client,
|
||||
Session: ses,
|
||||
ResponseType: responseType,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *AuthorizationUrl) Url() *url.URL {
|
||||
v := url.Values{
|
||||
"response_type": {a.ResponseType},
|
||||
"client_id": {a.ClientId},
|
||||
"redirect_uri": {a.RedirectUri},
|
||||
"scope": {strings.Join(a.Scopes, " ")},
|
||||
"state": {string(a.State)},
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: a.Host,
|
||||
Path: a.AuthorizationUrlPath,
|
||||
RawQuery: v.Encode(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthorizationUrl) String() string {
|
||||
return a.Url().String()
|
||||
}
|
32
client/client.go
Normal file
32
client/client.go
Normal file
@ -0,0 +1,32 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"somehole.com/common/oauth2/session"
|
||||
"somehole.com/common/security/signature"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
*IdentityProvider
|
||||
*signature.Keypair
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
RedirectUri string
|
||||
ResponseType string
|
||||
Scopes []string
|
||||
sessions map[session.SessionId]*session.Session
|
||||
}
|
||||
|
||||
func NewClient(idp *IdentityProvider, signer *signature.Keypair, id string, secret string, redirectUri string, responseType string, scopes []string) *Client {
|
||||
if signer == nil {
|
||||
signer, _ = signature.NewKeypair()
|
||||
}
|
||||
return &Client{
|
||||
IdentityProvider: idp,
|
||||
Keypair: signer,
|
||||
ClientId: id,
|
||||
ClientSecret: secret,
|
||||
RedirectUri: redirectUri,
|
||||
ResponseType: responseType,
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
17
client/provider.go
Normal file
17
client/provider.go
Normal file
@ -0,0 +1,17 @@
|
||||
package client
|
||||
|
||||
type IdentityProvider struct {
|
||||
Host string
|
||||
AuthorizationUrlPath string
|
||||
TokenUrlPath string
|
||||
TokenRevokationUrlPath string
|
||||
}
|
||||
|
||||
func NewIdentityProvider(host string, authorizationUrlPath string, tokenUrlPath string, tokenRevocationUrlPath string) *IdentityProvider {
|
||||
return &IdentityProvider{
|
||||
Host: host,
|
||||
AuthorizationUrlPath: authorizationUrlPath,
|
||||
TokenUrlPath: tokenUrlPath,
|
||||
TokenRevokationUrlPath: tokenUrlPath,
|
||||
}
|
||||
}
|
41
client/revokation.go
Normal file
41
client/revokation.go
Normal file
@ -0,0 +1,41 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"somehole.com/common/oauth2/session"
|
||||
)
|
||||
|
||||
type TokenRevokationUrl struct {
|
||||
*Client
|
||||
*session.Session
|
||||
TokenChoice session.TokenChoice
|
||||
}
|
||||
|
||||
func NewTokenRevokationUrl(client *Client, id session.SessionId, choice session.TokenChoice) (url *TokenRevokationUrl, err error) {
|
||||
ses, ok := client.sessions[id]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no session found")
|
||||
}
|
||||
url = &TokenRevokationUrl{
|
||||
Client: client,
|
||||
Session: ses,
|
||||
TokenChoice: choice,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TokenRevokationUrl) Url() *url.URL {
|
||||
v := url.Values{
|
||||
"token": {t.GetToken(t.TokenChoice)},
|
||||
"token_type_hint": {t.TokenChoice.String()},
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
User: url.UserPassword(t.ClientId, t.ClientSecret),
|
||||
Host: t.Host,
|
||||
Path: t.TokenUrlPath,
|
||||
RawQuery: v.Encode(),
|
||||
}
|
||||
}
|
50
client/token.go
Normal file
50
client/token.go
Normal file
@ -0,0 +1,50 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"somehole.com/common/oauth2/session"
|
||||
)
|
||||
|
||||
type TokenUrl struct {
|
||||
*Client
|
||||
*session.Session
|
||||
TokenChoice session.TokenChoice
|
||||
}
|
||||
|
||||
func NewTokenUrl(client *Client, id session.SessionId, choice session.TokenChoice) (url *TokenUrl, err error) {
|
||||
ses, ok := client.sessions[id]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no session found")
|
||||
}
|
||||
url = &TokenUrl{
|
||||
Client: client,
|
||||
Session: ses,
|
||||
TokenChoice: choice,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TokenUrl) Url() *url.URL {
|
||||
v := url.Values{}
|
||||
if t.TokenChoice == session.TokenChoiceAccess {
|
||||
v.Set("grant_type", "authorization_code")
|
||||
v.Set("code", string(t.Code))
|
||||
v.Set("redirect_uri", string(t.RedirectUri))
|
||||
} else if t.TokenChoice == session.TokenChoiceRefresh {
|
||||
v.Set("grant_type", "refresh_token")
|
||||
v.Set("refresh_token", string(t.RefreshToken))
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
User: url.UserPassword(t.ClientId, t.ClientSecret),
|
||||
Host: t.Host,
|
||||
Path: t.TokenUrlPath,
|
||||
RawQuery: v.Encode(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TokenUrl) String() string {
|
||||
return t.Url().String()
|
||||
}
|
15
go.mod
Normal file
15
go.mod
Normal file
@ -0,0 +1,15 @@
|
||||
module somehole.com/common/oauth2
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
somehole.com/common/defaults v0.1.2
|
||||
somehole.com/common/log v0.1.2
|
||||
somehole.com/common/security v0.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cloudflare/circl v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
)
|
12
go.sum
Normal file
12
go.sum
Normal file
@ -0,0 +1,12 @@
|
||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d h1:LiA25/KWKuXfIq5pMIBq1s5hz3HQxhJJSu/SUGlD+SM=
|
||||
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
somehole.com/common/defaults v0.1.2 h1:y523TtBEP1y415akn4DELk/7iNxNvawKDyKtQq9QdNk=
|
||||
somehole.com/common/defaults v0.1.2/go.mod h1:jPc/GeCBxJHwPJK8UUh3phDxZ1L9KCd9xXEyGSnRhCo=
|
||||
somehole.com/common/log v0.1.2 h1:e3rHAKL4IR7K79oEB4eNsrUNTad25H25GpPYYBJcDcw=
|
||||
somehole.com/common/log v0.1.2/go.mod h1:NS2eHnN120GA6oFbBm3XhB5yHww0eXTbLuMQYZxYNyA=
|
||||
somehole.com/common/security v0.1.0 h1:Mm4GvO+eV2/qxHSGF5vzcYziEj1SDsDt3c/Vs5+KMGE=
|
||||
somehole.com/common/security v0.1.0/go.mod h1:6SMKdIrfxT460XXWafpD6A9yT/ykwipGsGztA5g5vVM=
|
46
server.go
Normal file
46
server.go
Normal file
@ -0,0 +1,46 @@
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"somehole.com/common/log"
|
||||
"somehole.com/common/oauth2/server"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*http.ServeMux
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func NewServer(mux *http.ServeMux, logger log.Logger) *Server {
|
||||
if mux == nil {
|
||||
mux = http.NewServeMux()
|
||||
}
|
||||
return &Server{
|
||||
ServeMux: mux,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterCallbackServer(srv server.CallbackServer) {
|
||||
s.Handle(server.CallbackEndpoint, server.NewServer(&server.CallbackRequest{}, []string{http.MethodPost}, s.Logger, func(req server.Request) (res server.Response, errRes server.ErrorResponse) {
|
||||
callbackRequest, ok := req.(*server.CallbackRequest)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("expected CallbackRequest, got %T", req))
|
||||
}
|
||||
res, errRes = srv.Callback(callbackRequest)
|
||||
return
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Server) RegisterTokenServer(srv server.TokenServer) {
|
||||
s.Handle(server.TokenEndpoint, server.NewServer(&server.TokenRequest{}, []string{http.MethodPost}, s.Logger, func(req server.Request) (res server.Response, errRes server.ErrorResponse) {
|
||||
tokenRequest, ok := req.(*server.TokenRequest)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("expected TokenRequest, got %T", req))
|
||||
}
|
||||
res, errRes = srv.Token(tokenRequest)
|
||||
return
|
||||
}))
|
||||
}
|
112
server/callback.go
Normal file
112
server/callback.go
Normal file
@ -0,0 +1,112 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"somehole.com/common/oauth2/session"
|
||||
)
|
||||
|
||||
const CallbackEndpoint = "/callback"
|
||||
|
||||
type CallbackError uint32
|
||||
|
||||
const (
|
||||
CallbackOk CallbackError = iota
|
||||
CallbackErrorUnimplemented
|
||||
CallbackErrorUnauthorized
|
||||
CallbackErrorServerError
|
||||
)
|
||||
|
||||
func (ce *CallbackError) Ok() bool {
|
||||
return *ce == CallbackOk
|
||||
}
|
||||
|
||||
func (ce *CallbackError) HttpStatus() (code int) {
|
||||
switch *ce {
|
||||
case CallbackOk:
|
||||
code = http.StatusOK
|
||||
case CallbackErrorUnimplemented:
|
||||
code = http.StatusInternalServerError
|
||||
case CallbackErrorUnauthorized:
|
||||
code = http.StatusUnauthorized
|
||||
case CallbackErrorServerError:
|
||||
code = http.StatusInternalServerError
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ce *CallbackError) String() (out string) {
|
||||
switch *ce {
|
||||
case CallbackOk:
|
||||
out = "authenticated"
|
||||
case CallbackErrorUnimplemented:
|
||||
out = "callback server unimplemented"
|
||||
case CallbackErrorUnauthorized:
|
||||
out = "user unauthorized"
|
||||
case CallbackErrorServerError:
|
||||
out = "internal server error"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ce *CallbackError) ErrorResponse() []byte {
|
||||
var msg string
|
||||
switch *ce {
|
||||
default:
|
||||
msg = "internal_server_error"
|
||||
}
|
||||
return mustMarshalJson(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: msg,
|
||||
})
|
||||
}
|
||||
|
||||
type CallbackRequest struct {
|
||||
State session.State
|
||||
Code session.Code
|
||||
}
|
||||
|
||||
func (cr *CallbackRequest) Parse(data *url.Values) (err error) {
|
||||
if !data.Has("code") {
|
||||
err = fmt.Errorf("missing code paramater")
|
||||
return
|
||||
}
|
||||
if !data.Has("state") {
|
||||
err = fmt.Errorf("missing state parameter")
|
||||
return
|
||||
}
|
||||
cr.State = session.State(data.Get("state"))
|
||||
cr.Code = session.Code(data.Get("code"))
|
||||
return
|
||||
}
|
||||
|
||||
type CallbackResponse struct {
|
||||
Status int `json:"-"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (cr *CallbackResponse) HttpStatus() (code int) {
|
||||
return cr.Status
|
||||
}
|
||||
|
||||
func (cr *CallbackResponse) Response() []byte {
|
||||
return mustMarshalJson(cr)
|
||||
}
|
||||
|
||||
type UnimplementedCallbackServer struct{}
|
||||
|
||||
func (u UnimplementedCallbackServer) mustEmbedUnimplementedCallbackServer() {}
|
||||
|
||||
func (u UnimplementedCallbackServer) Callback(callback *CallbackRequest) (callbackResp *CallbackResponse, callbackErr *CallbackError) {
|
||||
ce := CallbackErrorUnimplemented
|
||||
callbackErr = &ce
|
||||
return
|
||||
}
|
||||
|
||||
type CallbackServer interface {
|
||||
mustEmbedUnimplementedCallbackServer()
|
||||
Callback(callback *CallbackRequest) (callbackResp *CallbackResponse, callbackErr *CallbackError)
|
||||
}
|
14
server/common.go
Normal file
14
server/common.go
Normal file
@ -0,0 +1,14 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func mustMarshalJson(in any) []byte {
|
||||
out, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not marshal %#v: %v", in, err))
|
||||
}
|
||||
return out
|
||||
}
|
78
server/server.go
Normal file
78
server/server.go
Normal file
@ -0,0 +1,78 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"somehole.com/common/defaults"
|
||||
"somehole.com/common/log"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
req Request
|
||||
allowed []string
|
||||
logger log.Logger
|
||||
do func(req Request) (res Response, errRes ErrorResponse)
|
||||
}
|
||||
|
||||
type Request interface {
|
||||
Parse(*url.Values) error
|
||||
}
|
||||
|
||||
type Response interface {
|
||||
HttpStatus() int
|
||||
Response() []byte
|
||||
}
|
||||
|
||||
type ErrorResponse interface {
|
||||
Ok() bool
|
||||
HttpStatus() int
|
||||
ErrorResponse() []byte
|
||||
String() string
|
||||
}
|
||||
|
||||
func NewServer(req Request, allowed []string, logger log.Logger, do func(req Request) (res Response, errRes ErrorResponse)) *server {
|
||||
return &server{
|
||||
req: req,
|
||||
allowed: allowed,
|
||||
logger: logger,
|
||||
do: do,
|
||||
}
|
||||
}
|
||||
|
||||
type defaultError struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (srv *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !defaults.Empty(srv.req) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write(mustMarshalJson(&defaultError{Error: "internal_server_error"}))
|
||||
srv.logger.Logf(log.LevelError, "expected empty server request template")
|
||||
return
|
||||
}
|
||||
req := srv.req
|
||||
var allowed bool
|
||||
for _, method := range srv.allowed {
|
||||
if method == r.Method {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
w.Write(mustMarshalJson(&defaultError{Error: "method_not_allowed"}))
|
||||
srv.logger.Logf(log.LevelError, "requested method (%s) not one of %v", r.Method, srv.allowed)
|
||||
return
|
||||
}
|
||||
r.ParseForm()
|
||||
req.Parse(&r.Form)
|
||||
res, errRes := srv.do(req)
|
||||
if !errRes.Ok() {
|
||||
w.WriteHeader(errRes.HttpStatus())
|
||||
w.Write(errRes.ErrorResponse())
|
||||
srv.logger.Logf(log.LevelError, "request failed: %s", errRes.String())
|
||||
}
|
||||
w.WriteHeader(res.HttpStatus())
|
||||
w.Write(res.Response())
|
||||
}
|
129
server/token.go
Normal file
129
server/token.go
Normal file
@ -0,0 +1,129 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"somehole.com/common/oauth2/session"
|
||||
)
|
||||
|
||||
const TokenEndpoint = "/token"
|
||||
|
||||
type TokenError uint32
|
||||
|
||||
const (
|
||||
TokenOk TokenError = iota
|
||||
TokenErrorUnimplemented
|
||||
TokenErrorUnauthorized
|
||||
TokenErrorServerError
|
||||
TokenErrorSlowDown
|
||||
TokenErrorPending
|
||||
)
|
||||
|
||||
func (te *TokenError) Ok() bool {
|
||||
return *te == TokenOk
|
||||
}
|
||||
|
||||
func (te *TokenError) HttpStatus() (code int) {
|
||||
switch *te {
|
||||
case TokenOk:
|
||||
code = http.StatusOK
|
||||
case TokenErrorUnimplemented:
|
||||
code = http.StatusInternalServerError
|
||||
case TokenErrorUnauthorized:
|
||||
code = http.StatusUnauthorized
|
||||
case TokenErrorServerError:
|
||||
code = http.StatusInternalServerError
|
||||
case TokenErrorSlowDown:
|
||||
code = http.StatusBadRequest
|
||||
case TokenErrorPending:
|
||||
code = http.StatusBadRequest
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (te *TokenError) String() (out string) {
|
||||
switch *te {
|
||||
case TokenOk:
|
||||
out = "authenticated"
|
||||
case TokenErrorUnimplemented:
|
||||
out = "token server unimplemented"
|
||||
case TokenErrorUnauthorized:
|
||||
out = "user unauthorized"
|
||||
case TokenErrorServerError:
|
||||
out = "internal server error"
|
||||
case TokenErrorSlowDown:
|
||||
out = "slow down"
|
||||
case TokenErrorPending:
|
||||
out = "authorization pending"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (te *TokenError) ErrorResponse() []byte {
|
||||
var msg string
|
||||
switch *te {
|
||||
case TokenErrorSlowDown:
|
||||
msg = "slow_down"
|
||||
case TokenErrorPending:
|
||||
msg = "authorization_pending"
|
||||
default:
|
||||
msg = "internal_server_error"
|
||||
}
|
||||
return mustMarshalJson(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: msg,
|
||||
})
|
||||
}
|
||||
|
||||
type TokenRequest struct {
|
||||
State session.State
|
||||
Code session.Code
|
||||
}
|
||||
|
||||
func (tr *TokenRequest) Parse(data *url.Values) (err error) {
|
||||
if !data.Has("code") {
|
||||
err = fmt.Errorf("missing code paramater")
|
||||
return
|
||||
}
|
||||
if !data.Has("state") {
|
||||
err = fmt.Errorf("missing state parameter")
|
||||
return
|
||||
}
|
||||
tr.State = session.State(data.Get("state"))
|
||||
tr.Code = session.Code(data.Get("code"))
|
||||
return
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Status int `json:"-"`
|
||||
VerificationUri string `json:"verification_uri"`
|
||||
UserCode session.Code `json:"user_code"`
|
||||
DeviceCode session.Code `json:"device_code"`
|
||||
Interval uint8 `json:"interval"`
|
||||
}
|
||||
|
||||
func (tr *TokenResponse) HttpStatus() (code int) {
|
||||
return tr.Status
|
||||
}
|
||||
|
||||
func (tr *TokenResponse) Response() []byte {
|
||||
return mustMarshalJson(tr)
|
||||
}
|
||||
|
||||
type UnimplementedTokenServer struct{}
|
||||
|
||||
func (u UnimplementedTokenServer) mustEmbedUnimplementedTokenServer() {}
|
||||
|
||||
func (u UnimplementedTokenServer) Token(token *TokenRequest) (tokenResp *TokenResponse, tokenErr *TokenError) {
|
||||
te := TokenErrorUnimplemented
|
||||
tokenErr = &te
|
||||
return
|
||||
}
|
||||
|
||||
type TokenServer interface {
|
||||
mustEmbedUnimplementedTokenServer()
|
||||
Token(token *TokenRequest) (tokenResp *TokenResponse, tokenErr *TokenError)
|
||||
}
|
74
session/session.go
Normal file
74
session/session.go
Normal file
@ -0,0 +1,74 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"somehole.com/common/security/signature"
|
||||
)
|
||||
|
||||
type SessionId string
|
||||
|
||||
func NewSessionId() SessionId {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
return SessionId(hex.EncodeToString(b))
|
||||
}
|
||||
|
||||
type State string
|
||||
|
||||
func NewState(id SessionId, signer *signature.Keypair) State {
|
||||
sig, err := signer.Sign([]byte(id))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return State(hex.EncodeToString(sig[:]))
|
||||
}
|
||||
|
||||
type Code string
|
||||
|
||||
type AccessToken string
|
||||
|
||||
type RefreshToken string
|
||||
|
||||
type TokenChoice uint8
|
||||
|
||||
const (
|
||||
TokenChoiceAccess TokenChoice = iota
|
||||
TokenChoiceRefresh
|
||||
)
|
||||
|
||||
func (t TokenChoice) String() (out string) {
|
||||
switch t {
|
||||
case TokenChoiceAccess:
|
||||
out = "access_token"
|
||||
case TokenChoiceRefresh:
|
||||
out = "refresh_token"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
SessionId SessionId
|
||||
State State
|
||||
Code Code
|
||||
AccessToken AccessToken
|
||||
RefreshToken RefreshToken
|
||||
}
|
||||
|
||||
func NewSession() *Session {
|
||||
id := NewSessionId()
|
||||
return &Session{
|
||||
SessionId: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) GetToken(choice TokenChoice) (token string) {
|
||||
switch choice {
|
||||
case TokenChoiceAccess:
|
||||
token = string(s.AccessToken)
|
||||
case TokenChoiceRefresh:
|
||||
token = string(s.RefreshToken)
|
||||
}
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue
Block a user