Commit 79a4e0b2 authored by Simon Schürg's avatar Simon Schürg 🚀
Browse files

Refactoring, userinfo command and internal cleanup

parent 3fdc8a3b
......@@ -86,6 +86,15 @@ var loginROPCmd = &cobra.Command{
},
}
var loginImplicitCmd = &cobra.Command{
Use: "implicit",
Short: "Performs OpenID Connect Implicit Authorization",
Long: `Performs OpenID Connect Implicit Authorization`,
Run: func(cmd *cobra.Command, args []string) {
internal.ImplicitAuth(issuer, clientID, clientSecret)
},
}
var loginCCCmd = &cobra.Command{
Use: "client",
Short: "Performs OpenID Connect Client Credentials Authorization",
......@@ -135,6 +144,8 @@ func init() {
loginROPCmd.Flags().StringVar(&username, "username", "", "Username")
loginROPCmd.Flags().StringVar(&password, "password", "", "Password")
loginCmd.AddCommand(loginImplicitCmd)
loginCmd.AddCommand(refreshCmd)
refreshCmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect Issuer")
refreshCmd.Flags().StringVar(&clientID, "client_id", "", "Client ID")
......
......@@ -18,9 +18,9 @@ var (
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "actl",
Short: "A brief description of your application",
Short: "A CLI tool for OAuth 2.0 and OpenID Connect based access control.",
Long: `actl - Access Control
A CLI tool for OAuth and OpenID Connect based access control.`,
A CLI tool for OAuth 2.0 and OpenID Connect based access control.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
......
package cmd
import (
"git.schuerg.net/simon/actl/internal"
"github.com/spf13/cobra"
)
var userinfoCmd = &cobra.Command{
Use: "userinfo [access_token]",
Short: "Fetches the OpenID Connect userinfo endpoint and returns the result",
Long: `Fetches the OpenID Connect userinfo endpoint and returns the result`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
rawToken := args[0]
internal.UpsertJWT(rawToken)
userinfoToken := internal.UserInfo(rawToken)
internal.PrettyPrintDecodedJWT(userinfoToken.Encoded)
},
}
func init() {
rootCmd.AddCommand(userinfoCmd)
}
......@@ -3,6 +3,7 @@ package internal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
......@@ -19,41 +20,51 @@ import (
"golang.org/x/sync/errgroup"
)
func RefreshToken(issuer, clientID, refreshToken string) {
// RefreshToken uses an existing refresh token to retrieve a new TokenSet
// See https://tools.ietf.org/html/rfc6749#section-6
func RefreshToken(issuer, clientID, refreshToken string) *TokenSet {
provider := GetOidcMetadata(issuer, true)
payload := url.Values{}
payload.Add("grant_type", "refresh_token")
payload.Add("client_id", clientID)
payload.Add("refresh_token", refreshToken)
req, err1 := http.NewRequest("POST", provider.TokenEndpoint, bytes.NewBufferString(payload.Encode()))
FatalOnError(err1)
req, err := http.NewRequest("POST", provider.TokenEndpoint, bytes.NewBufferString(payload.Encode()))
FatalOnError(err)
req.Header.Add("content-type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
FatalOnError(err)
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
FatalOnError(err)
PrettyPrintJSON(body)
var tokenSet TokenSet
json.Unmarshal(body, &tokenSet)
return &tokenSet
}
func ClientCredenitalsAuth(issuer, clientID, clientSecret string) {
// ClientCredenitalsAuth uses a client id and client secret to retrieve a TokenSet
// See https://tools.ietf.org/html/rfc6749#section-4.4
func ClientCredenitalsAuth(issuer, clientID, clientSecret string) *TokenSet {
provider := GetOidcMetadata(issuer, true)
payload := url.Values{}
payload.Add("grant_type", "client_credentials")
payload.Add("client_id", clientID)
payload.Add("client_secret", clientSecret)
req, err1 := http.NewRequest("POST", provider.TokenEndpoint, bytes.NewBufferString(payload.Encode()))
FatalOnError(err1)
req, err := http.NewRequest("POST", provider.TokenEndpoint, bytes.NewBufferString(payload.Encode()))
FatalOnError(err)
req.Header.Add("content-type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
FatalOnError(err)
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
FatalOnError(err)
PrettyPrintJSON(body)
var tokenSet TokenSet
json.Unmarshal(body, &tokenSet)
return &tokenSet
}
func ResourceOwnerCredentialsAuth(issuer, clientID, username, password string) {
// ResourceOwnerCredentialsAuth uses a username and password to retrieve a TokenSet
// See https://tools.ietf.org/html/rfc6749#section-10.7
func ResourceOwnerCredentialsAuth(issuer, clientID, username, password string) *TokenSet {
provider := GetOidcMetadata(issuer, true)
payload := url.Values{}
payload.Add("grant_type", "password")
......@@ -68,10 +79,14 @@ func ResourceOwnerCredentialsAuth(issuer, clientID, username, password string) {
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
FatalOnError(err)
PrettyPrintJSON(body)
var tokenSet TokenSet
json.Unmarshal(body, &tokenSet)
return &tokenSet
}
func AuthorizationCodeAuth(clientID, clientSecret, openidIssuerURL string) *Token {
// AuthorizationCodeAuth is a redirect based authentication flow to retrieve a TokenSet
// See https://tools.ietf.org/html/rfc6749#section-4.1
func AuthorizationCodeAuth(clientID, clientSecret, openidIssuerURL string) *TokenSet {
pkce, err := oauth2params.NewPKCE()
FatalOnError(errors.Wrap(err, "Failed creating a new PKCE"))
ready := make(chan string, 1)
......@@ -120,6 +135,11 @@ func AuthorizationCodeAuth(clientID, clientSecret, openidIssuerURL string) *Toke
UpsertJWT(token.AccessToken)
UpsertJWT(token.RefreshToken)
clipboard.WriteAll(token.AccessToken)
// tokenSet := TokenSet{
// AccessToken: token.AccessToken,
// RefreshToken: token.RefreshToken,
// TokenType: token.TokenType,
// }
return nil
})
if err = eg.Wait(); err != nil {
......@@ -127,3 +147,47 @@ func AuthorizationCodeAuth(clientID, clientSecret, openidIssuerURL string) *Toke
}
return nil
}
// ImplicitAuth is a redirect based authentication flow without support
// for refresh tokens.
// See https://tools.ietf.org/html/rfc6749#section-4.2
func ImplicitAuth(clientID, clientSecret, openidIssuerURL string) *Token {
log.Fatalln("Not yet implemented")
return nil
}
// Logout performs a logout based on the OpenID Connect "end_session_endpoint".
// The spec of "end_session_endpoint" is still a draft and could be changed in future.
// Therefore, it is advisable not to depend too much on it :-)
// See https://openid.net/specs/openid-connect-session-1_0.html
func Logout(issuer string) {
// oidcMeta := GetOidcMetadata(issuer, true)
}
// IntrospectToken implements token introspection as defined in RFC7662
// See https://tools.ietf.org/html/rfc7662
func IntrospectToken(token string) {}
// TokenRevocation as defined in RFC7009.
// Not yet supported by a wide range of OIDC providers.
// See https://tools.ietf.org/html/rfc7009
func TokenRevocation() {
log.Fatalln("Not yet implemented!")
}
// UserInfo fetches the user info OIDC endpoint and returns the result.
// The result is a userinfo token -- also a JWT.
func UserInfo(accessToken string) *Token {
token := DecodeToken([]byte(accessToken))
oidcMeta := GetOidcMetadata(token.GetRegisteredClaims().Issuer, true)
req, err := http.NewRequest("GET", oidcMeta.UserinfoEndpoint, nil)
FatalOnError(err)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
client := &http.Client{}
resp, err := client.Do(req)
FatalOnError(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
FatalOnError(err)
return DecodeToken(body)
}
......@@ -78,7 +78,10 @@ func UpsertJWT(encodedToken string) *Token {
return &token
}
// ClearDb closes and deletes the whole database.
// Be careful with this method, it is dangerous!
func ClearDb() {
CloseDb()
err := os.Remove(dbPath)
FatalOnError(err)
}
......@@ -97,6 +100,7 @@ func UpsertIssuer(oidcMetadata *OpenIDProviderMetadata) *OpenIDProviderMetadata
return oidcMetadata
}
// GetAllJWT fetches all known JWTs from the local database
func GetAllJWT() []Token {
var allToken []Token
err := db.AllByIndex("Encoded", &allToken)
......@@ -104,6 +108,7 @@ func GetAllJWT() []Token {
return allToken
}
// GetAllJWK fetches all known JWKs from the local database
func GetAllJWK() []JWK {
var allJWK []JWK
err := db.AllByIndex("Kid", &allJWK)
......@@ -111,6 +116,7 @@ func GetAllJWK() []JWK {
return allJWK
}
// GetAllIssuer fetches all known OpenID Connect issuers from the local database
func GetAllIssuer() []OpenIDProviderMetadata {
var allIssuer []OpenIDProviderMetadata
err := db.AllByIndex("Issuer", &allIssuer)
......
......@@ -2,6 +2,7 @@ package internal
import (
"encoding/json"
"fmt"
"log"
"github.com/TylerBrock/colorjson"
......@@ -16,7 +17,7 @@ func PrettyPrintJSON(jsonBytes []byte) {
var obj map[string]interface{}
json.Unmarshal(jsonBytes, &obj)
prettyHeader, _ := f.Marshal(obj)
println(string(prettyHeader))
fmt.Println(string(prettyHeader))
}
// PrettyPrintDecodedJWT parses base64 encoded JWT and prints
......@@ -26,8 +27,8 @@ func PrettyPrintDecodedJWT(token string) {
if err != nil {
log.Fatalln("Invalid Token. Could not be parsed.")
}
println("Header:")
fmt.Println("Header:")
PrettyPrintJSON(claims.RawHeader)
println("Payload:")
fmt.Println("Payload:")
PrettyPrintJSON(claims.Raw)
}
......@@ -4,15 +4,36 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/asdine/storm/v3"
"github.com/pascaldekloe/jwt"
"github.com/pkg/errors"
)
// TokenErrorResponse is the response type of an unsuccessful request against
// the OpenID Connect endpoints as defined in RFC6749.
// See https://tools.ietf.org/html/rfc6749#section-5.2
type TokenErrorResponse struct {
Error string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
ErrorURI string `json:"error_uri,omitempty"`
}
// TokenSet is the successful response of issuing an access token as defined by RFC6749.
// See https://tools.ietf.org/html/rfc6749#section-5.1
type TokenSet struct {
AccessToken string `json:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
}
// OpenIDProviderMetadata is the description of the OpenID Providers configuration.
// This information can be fetched from a well known URL.
// See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
......@@ -117,6 +138,27 @@ type Token struct {
Payload map[string]interface{}
}
// DecodeToken constructs a Token type object from a raw base64 JWT
func DecodeToken(rawToken []byte) *Token {
claims, err := jwt.ParseWithoutCheck(rawToken)
if err != nil {
log.Fatalln("Invalid Token. Could not be parsed.")
}
var header map[string]interface{}
err = json.Unmarshal(claims.RawHeader, &header)
FatalOnError(err)
var payload map[string]interface{}
err = json.Unmarshal(claims.Raw, &payload)
FatalOnError(err)
token := Token{
Encoded: string(rawToken),
Header: header,
Payload: payload,
}
return &token
}
// GetJOSEHeader returns the JOSE Header information from this token.
func (t *Token) GetJOSEHeader() *JOSEHeader {
b, err := json.Marshal(t.Header)
FatalOnError(errors.Wrap(err, "Error marshaling jwt headers to json"))
......@@ -126,6 +168,7 @@ func (t *Token) GetJOSEHeader() *JOSEHeader {
return &joseHeader
}
// GetRegisteredClaims returns the registered claims from this token.
func (t *Token) GetRegisteredClaims() *JWTRegisteredClaims {
b, err := json.Marshal(t.Payload)
FatalOnError(errors.Wrap(err, "Error marshaling jwt payload to json"))
......@@ -135,6 +178,7 @@ func (t *Token) GetRegisteredClaims() *JWTRegisteredClaims {
return &jwtRegClaims
}
// GetOidcStandardClaims returns the OpenID Connect standard claims from this token.
func (t *Token) GetOidcStandardClaims() *OpenIDStandardClaims {
b, err := json.Marshal(t.Payload)
FatalOnError(errors.Wrap(err, "Error marshaling jwt payload to json"))
......@@ -144,6 +188,9 @@ func (t *Token) GetOidcStandardClaims() *OpenIDStandardClaims {
return &oidcStdClaims
}
// JWTRegisteredClaims is a struct containing all registered JWT claims defined
// by RFC7519.
// See https://tools.ietf.org/html/rfc7519#section-4.1
type JWTRegisteredClaims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
......@@ -154,6 +201,8 @@ type JWTRegisteredClaims struct {
JWTID string `json:"jit"`
}
// OpenIDStandardClaims is a struct containing all standard claims defined by
// the openid spec.
// See https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
type OpenIDStandardClaims struct {
Subject string `json:"sub"`
......@@ -178,6 +227,8 @@ type OpenIDStandardClaims struct {
UpdatedAt string `json:"updated_at"`
}
// OpenIDAddressClaim is a struct containing the address datatype
// as defined in the openid spec.
// See https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim
type OpenIDAddressClaim struct {
Formatted string `json:"formatted"`
......@@ -234,6 +285,7 @@ func fetchJWKSet(issuer string) *JWKSet {
return &jwkSet
}
// GetOidcMetadata returns the OpenID Connect Provider Metadata of a given issuer
func GetOidcMetadata(issuer string, useCache bool) *OpenIDProviderMetadata {
var oidcMetadata OpenIDProviderMetadata
if useCache {
......@@ -249,6 +301,7 @@ func GetOidcMetadata(issuer string, useCache bool) *OpenIDProviderMetadata {
return &oidcMetadata
}
// GetJWK searches for a JWK by a given kid (key id)
func GetJWK(kid, issuer string, useCache bool) *JWK {
var jwk JWK
if useCache {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment