@ -1,14 +1,25 @@
package server
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
jose "gopkg.in/square/go-jose.v2"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/storage"
)
@ -125,6 +136,88 @@ func parseScopes(scopes []string) connector.Scopes {
return s
}
// Determine the signature algorithm for a JWT.
func signatureAlgorithm ( jwk * jose . JSONWebKey ) ( alg jose . SignatureAlgorithm , err error ) {
if jwk . Key == nil {
return alg , errors . New ( "no signing key" )
}
switch key := jwk . Key . ( type ) {
case * rsa . PrivateKey :
// Because OIDC mandates that we support RS256, we always return that
// value. In the future, we might want to make this configurable on a
// per client basis. For example allowing PS256 or ECDSA variants.
//
// See https://github.com/coreos/dex/issues/692
return jose . RS256 , nil
case * ecdsa . PrivateKey :
// We don't actually support ECDSA keys yet, but they're tested for
// in case we want to in the future.
//
// These values are prescribed depending on the ECDSA key type. We
// can't return different values.
switch key . Params ( ) {
case elliptic . P256 ( ) . Params ( ) :
return jose . ES256 , nil
case elliptic . P384 ( ) . Params ( ) :
return jose . ES384 , nil
case elliptic . P521 ( ) . Params ( ) :
return jose . ES512 , nil
default :
return alg , errors . New ( "unsupported ecdsa curve" )
}
default :
return alg , fmt . Errorf ( "unsupported signing key type %T" , key )
}
}
func signPayload ( key * jose . JSONWebKey , alg jose . SignatureAlgorithm , payload [ ] byte ) ( jws string , err error ) {
signingKey := jose . SigningKey { Key : key , Algorithm : alg }
signer , err := jose . NewSigner ( signingKey , & jose . SignerOptions { } )
if err != nil {
return "" , fmt . Errorf ( "new signier: %v" , err )
}
signature , err := signer . Sign ( payload )
if err != nil {
return "" , fmt . Errorf ( "signing payload: %v" , err )
}
return signature . CompactSerialize ( )
}
// The hash algorithm for the at_hash is detemrined by the signing
// algorithm used for the id_token. From the spec:
//
// ...the hash algorithm used is the hash algorithm used in the alg Header
// Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256,
// hash the access_token value with SHA-256
//
// https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
var hashForSigAlg = map [ jose . SignatureAlgorithm ] func ( ) hash . Hash {
jose . RS256 : sha256 . New ,
jose . RS384 : sha512 . New384 ,
jose . RS512 : sha512 . New ,
jose . ES256 : sha256 . New ,
jose . ES384 : sha512 . New384 ,
jose . ES512 : sha512 . New ,
}
// Compute an at_hash from a raw access token and a signature algorithm
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
func accessTokenHash ( alg jose . SignatureAlgorithm , accessToken string ) ( string , error ) {
newHash , ok := hashForSigAlg [ alg ]
if ! ok {
return "" , fmt . Errorf ( "unsupported signature algorithm: %s" , alg )
}
hash := newHash ( )
if _ , err := io . WriteString ( hash , accessToken ) ; err != nil {
return "" , fmt . Errorf ( "computing hash: %v" , err )
}
sum := hash . Sum ( nil )
return base64 . RawURLEncoding . EncodeToString ( sum [ : len ( sum ) / 2 ] ) , nil
}
type audience [ ] string
func ( a audience ) MarshalJSON ( ) ( [ ] byte , error ) {
@ -143,6 +236,8 @@ type idTokenClaims struct {
AuthorizingParty string ` json:"azp,omitempty" `
Nonce string ` json:"nonce,omitempty" `
AccessTokenHash string ` json:"at_hash,omitempty" `
Email string ` json:"email,omitempty" `
EmailVerified * bool ` json:"email_verified,omitempty" `
@ -151,7 +246,22 @@ type idTokenClaims struct {
Name string ` json:"name,omitempty" `
}
func ( s * Server ) newIDToken ( clientID string , claims storage . Claims , scopes [ ] string , nonce string ) ( idToken string , expiry time . Time , err error ) {
func ( s * Server ) newIDToken ( clientID string , claims storage . Claims , scopes [ ] string , nonce , accessToken string ) ( idToken string , expiry time . Time , err error ) {
keys , err := s . storage . GetKeys ( )
if err != nil {
s . logger . Errorf ( "Failed to get keys: %v" , err )
return "" , expiry , err
}
signingKey := keys . SigningKey
if signingKey == nil {
return "" , expiry , fmt . Errorf ( "no key to sign payload with" )
}
signingAlg , err := signatureAlgorithm ( signingKey )
if err != nil {
return "" , expiry , err
}
issuedAt := s . now ( )
expiry = issuedAt . Add ( s . idTokensValidFor )
@ -163,6 +273,15 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
IssuedAt : issuedAt . Unix ( ) ,
}
if accessToken != "" {
atHash , err := accessTokenHash ( signingAlg , accessToken )
if err != nil {
s . logger . Errorf ( "error computing at_hash: %v" , err )
return "" , expiry , fmt . Errorf ( "error computing at_hash: %v" , err )
}
tok . AccessTokenHash = atHash
}
for _ , scope := range scopes {
switch {
case scope == scopeEmail :
@ -175,6 +294,8 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
default :
peerID , ok := parseCrossClientScope ( scope )
if ! ok {
// Ignore unknown scopes. These are already validated during the
// initial auth request.
continue
}
isTrusted , err := s . validateCrossClientTrust ( clientID , peerID )
@ -188,9 +309,14 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
tok . Audience = append ( tok . Audience , peerID )
}
}
if len ( tok . Audience ) == 0 {
// Client didn't ask for cross client audience. Set the current
// client as the audience.
tok . Audience = audience { clientID }
} else {
// Client asked for cross client audience. The current client
// becomes the authorizing party.
tok . AuthorizingParty = clientID
}
@ -199,12 +325,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
return "" , expiry , fmt . Errorf ( "could not serialize claims: %v" , err )
}
keys , err := s . storage . GetKeys ( )
if err != nil {
s . logger . Errorf ( "Failed to get keys: %v" , err )
return "" , expiry , err
}
if idToken , err = keys . Sign ( payload ) ; err != nil {
if idToken , err = signPayload ( signingKey , signingAlg , payload ) ; err != nil {
return "" , expiry , fmt . Errorf ( "failed to sign payload: %v" , err )
}
return idToken , expiry , nil