274 lines
8.6 KiB
Go
274 lines
8.6 KiB
Go
/*
|
|
An example of adding OpenID Connect support to osin.
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/RangelReale/osin"
|
|
"github.com/RangelReale/osin/example"
|
|
|
|
"gopkg.in/square/go-jose.v1"
|
|
)
|
|
|
|
var (
|
|
issuer = "http://127.0.0.1:14001"
|
|
server = osin.NewServer(osin.NewServerConfig(), example.NewTestStorage())
|
|
|
|
jwtSigner jose.Signer
|
|
publicKeys *jose.JsonWebKeySet
|
|
)
|
|
|
|
func main() {
|
|
// Load signing key.
|
|
block, _ := pem.Decode(privateKeyBytes)
|
|
if block == nil {
|
|
log.Fatalf("no private key found")
|
|
}
|
|
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
log.Fatalf("failed to parse key: %v", err)
|
|
}
|
|
|
|
// Configure jwtSigner and public keys.
|
|
privateKey := &jose.JsonWebKey{
|
|
Key: key,
|
|
Algorithm: "RS256",
|
|
Use: "sig",
|
|
KeyID: "1", // KeyID should use the key thumbprint.
|
|
}
|
|
|
|
jwtSigner, err = jose.NewSigner(jose.RS256, privateKey)
|
|
if err != nil {
|
|
log.Fatalf("failed to create jwtSigner: %v", err)
|
|
}
|
|
publicKeys = &jose.JsonWebKeySet{
|
|
Keys: []jose.JsonWebKey{
|
|
jose.JsonWebKey{Key: &key.PublicKey,
|
|
Algorithm: "RS256",
|
|
Use: "sig",
|
|
KeyID: "1",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Register the four manditory OpenID Connect endpoints: discovery, public keys, auth, and token.
|
|
http.HandleFunc("/.well-known/openid-configuration", handleDiscovery)
|
|
http.HandleFunc("/publickeys", handlePublicKeys)
|
|
http.HandleFunc("/authorize", handleAuthorization)
|
|
http.HandleFunc("/token", handleToken)
|
|
|
|
log.Fatal(http.ListenAndServe("127.0.0.1:14001", nil))
|
|
}
|
|
|
|
// The ID Token represents a JWT passed to the client as part of the token response.
|
|
//
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
|
type IDToken struct {
|
|
Issuer string `json:"iss"`
|
|
UserID string `json:"sub"`
|
|
ClientID string `json:"aud"`
|
|
Expiration int64 `json:"exp"`
|
|
IssuedAt int64 `json:"iat"`
|
|
|
|
Nonce string `json:"nonce,omitempty"` // Non-manditory fields MUST be "omitempty"
|
|
|
|
// Custom claims supported by this server.
|
|
//
|
|
// See: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
|
|
|
Email string `json:"email,omitempty"`
|
|
EmailVerified *bool `json:"email_verified,omitempty"`
|
|
|
|
Name string `json:"name,omitempty"`
|
|
FamilyName string `json:"family_name,omitempty"`
|
|
GivenName string `json:"given_name,omitempty"`
|
|
Locale string `json:"locale,omitempty"`
|
|
}
|
|
|
|
// handleDiscovery returns the OpenID Connect discovery object, allowing clients
|
|
// to discover OAuth2 resources.
|
|
func handleDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
// For other example see: https://accounts.google.com/.well-known/openid-configuration
|
|
data := map[string]interface{}{
|
|
"issuer": issuer,
|
|
"authorization_endpoint": issuer + "/authorize",
|
|
"token_endpoint": issuer + "/token",
|
|
"jwks_uri": issuer + "/publickeys",
|
|
"response_types_supported": []string{"code"},
|
|
"subject_types_supported": []string{"public"},
|
|
"id_token_signing_alg_values_supported": []string{"RS256"},
|
|
"scopes_supported": []string{"openid", "email", "profile"},
|
|
"token_endpoint_auth_methods_supported": []string{"client_secret_basic"},
|
|
"claims_supported": []string{
|
|
"aud", "email", "email_verified", "exp",
|
|
"family_name", "given_name", "iat", "iss",
|
|
"locale", "name", "sub",
|
|
},
|
|
}
|
|
|
|
raw, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
log.Printf("failed to marshal data: %v", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(raw)))
|
|
w.Write(raw)
|
|
}
|
|
|
|
// handlePublicKeys publishes the public part of this server's signing keys.
|
|
// This allows clients to verify the signature of ID Tokens.
|
|
func handlePublicKeys(w http.ResponseWriter, r *http.Request) {
|
|
raw, err := json.MarshalIndent(publicKeys, "", " ")
|
|
if err != nil {
|
|
log.Printf("failed to marshal data: %v", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(raw)))
|
|
w.Write(raw)
|
|
}
|
|
|
|
func handleAuthorization(w http.ResponseWriter, r *http.Request) {
|
|
resp := server.NewResponse()
|
|
defer resp.Close()
|
|
|
|
if ar := server.HandleAuthorizeRequest(resp, r); ar != nil {
|
|
if !example.HandleLoginPage(ar, w, r) {
|
|
return
|
|
}
|
|
|
|
ar.Authorized = true
|
|
scopes := make(map[string]bool)
|
|
for _, s := range strings.Fields(ar.Scope) {
|
|
scopes[s] = true
|
|
}
|
|
|
|
// If the "openid" connect scope is specified, attach an ID Token to the
|
|
// authorization response.
|
|
//
|
|
// The ID Token will be serialized and signed during the code for token exchange.
|
|
if scopes["openid"] {
|
|
|
|
// These values would be tied to the end user authorizing the client.
|
|
now := time.Now()
|
|
idToken := IDToken{
|
|
Issuer: issuer,
|
|
UserID: "id-of-test-user",
|
|
ClientID: ar.Client.GetId(),
|
|
Expiration: now.Add(time.Hour).Unix(),
|
|
IssuedAt: now.Unix(),
|
|
Nonce: r.URL.Query().Get("nonce"),
|
|
}
|
|
|
|
if scopes["profile"] {
|
|
idToken.Name = "Jane Doe"
|
|
idToken.GivenName = "Jane"
|
|
idToken.FamilyName = "Doe"
|
|
idToken.Locale = "us"
|
|
}
|
|
|
|
if scopes["email"] {
|
|
t := true
|
|
idToken.Email = "jane.doe@example.com"
|
|
idToken.EmailVerified = &t
|
|
}
|
|
// NOTE: The storage must be able to encode and decode this object.
|
|
ar.UserData = &idToken
|
|
}
|
|
server.FinishAuthorizeRequest(resp, r, ar)
|
|
}
|
|
|
|
if resp.IsError && resp.InternalError != nil {
|
|
log.Printf("internal error: %v", resp.InternalError)
|
|
}
|
|
osin.OutputJSON(resp, w, r)
|
|
}
|
|
|
|
func handleToken(w http.ResponseWriter, r *http.Request) {
|
|
resp := server.NewResponse()
|
|
defer resp.Close()
|
|
|
|
if ar := server.HandleAccessRequest(resp, r); ar != nil {
|
|
ar.Authorized = true
|
|
server.FinishAccessRequest(resp, r, ar)
|
|
|
|
// If an ID Token was encoded as the UserData, serialize and sign it.
|
|
if idToken, ok := ar.UserData.(*IDToken); ok && idToken != nil {
|
|
encodeIDToken(resp, idToken, jwtSigner)
|
|
}
|
|
}
|
|
if resp.IsError && resp.InternalError != nil {
|
|
fmt.Printf("ERROR: %s\n", resp.InternalError)
|
|
}
|
|
osin.OutputJSON(resp, w, r)
|
|
}
|
|
|
|
// encodeIDToken serializes and signs an ID Token then adds a field to the token response.
|
|
func encodeIDToken(resp *osin.Response, idToken *IDToken, singer jose.Signer) {
|
|
resp.InternalError = func() error {
|
|
payload, err := json.Marshal(idToken)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal token: %v", err)
|
|
}
|
|
jws, err := jwtSigner.Sign(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to sign token: %v", err)
|
|
}
|
|
raw, err := jws.CompactSerialize()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize token: %v", err)
|
|
}
|
|
resp.Output["id_token"] = raw
|
|
return nil
|
|
}()
|
|
|
|
// Record errors as internal server errors.
|
|
if resp.InternalError != nil {
|
|
resp.IsError = true
|
|
resp.ErrorId = osin.E_SERVER_ERROR
|
|
}
|
|
}
|
|
|
|
var (
|
|
privateKeyBytes = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn
|
|
SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i
|
|
cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC
|
|
PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR
|
|
ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA
|
|
Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3
|
|
n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy
|
|
MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9
|
|
POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE
|
|
KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM
|
|
IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn
|
|
FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY
|
|
mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj
|
|
FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U
|
|
I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs
|
|
2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn
|
|
/iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT
|
|
OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86
|
|
EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+
|
|
hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0
|
|
4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb
|
|
mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry
|
|
eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3
|
|
CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+
|
|
9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq
|
|
-----END RSA PRIVATE KEY-----`)
|
|
)
|