Skip to content
Snippets Groups Projects
supertoken.go 7.81 KiB
Newer Older
Gabriel Zachmann's avatar
Gabriel Zachmann committed
package supertoken

import (
	"database/sql/driver"
	"fmt"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/jmoiron/sqlx"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	uuid "github.com/satori/go.uuid"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	"github.com/zachmann/mytoken/internal/config"
	"github.com/zachmann/mytoken/internal/db"
	"github.com/zachmann/mytoken/internal/db/dbrepo/supertokenrepo/transfercoderepo"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	response "github.com/zachmann/mytoken/internal/endpoints/token/super/pkg"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	"github.com/zachmann/mytoken/internal/jws"
	"github.com/zachmann/mytoken/internal/model"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	"github.com/zachmann/mytoken/internal/supertoken/capabilities"
	eventService "github.com/zachmann/mytoken/internal/supertoken/event"
	event "github.com/zachmann/mytoken/internal/supertoken/event/pkg"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	"github.com/zachmann/mytoken/internal/supertoken/restrictions"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	"github.com/zachmann/mytoken/internal/utils/issuerUtils"
Gabriel Zachmann's avatar
Gabriel Zachmann committed
)

// SuperToken is a mytoken SuperToken
type SuperToken struct {
	Issuer               string                    `json:"iss"`
	Subject              string                    `json:"sub"`
	ExpiresAt            int64                     `json:"exp,omitempty"`
	NotBefore            int64                     `json:"nbf"`
	IssuedAt             int64                     `json:"iat"`
	ID                   uuid.UUID                 `json:"jti"`
	Audience             string                    `json:"aud"`
	OIDCSubject          string                    `json:"oidc_sub"`
	OIDCIssuer           string                    `json:"oidc_iss"`
	Restrictions         restrictions.Restrictions `json:"restrictions,omitempty"`
	Capabilities         capabilities.Capabilities `json:"capabilities"`
	SubtokenCapabilities capabilities.Capabilities `json:"subtoken_capabilities,omitempty"`
	jwt                  string
Gabriel Zachmann's avatar
Gabriel Zachmann committed
}

func (st *SuperToken) verifyID() bool {
	if len(st.ID.String()) == 0 {
		return false
	}
	return true
}

func (st *SuperToken) verifySubject() bool {
	if len(st.Subject) == 0 {
		return false
	}
	if st.Subject != issuerUtils.CombineSubIss(st.OIDCSubject, st.OIDCIssuer) {
		return false
	}
	return true
}

// VerifyCapabilities verifies that this super token has the required capabilities
func (st *SuperToken) VerifyCapabilities(required ...capabilities.Capability) bool {
	if st.Capabilities == nil || len(st.Capabilities) == 0 {
		return false
	}
	for _, c := range required {
		if !st.Capabilities.Has(c) {
			return false
		}
	}
	return true
}

Gabriel Zachmann's avatar
Gabriel Zachmann committed
// NewSuperToken creates a new SuperToken
func NewSuperToken(oidcSub, oidcIss string, r restrictions.Restrictions, c, sc capabilities.Capabilities) *SuperToken {
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	now := time.Now().Unix()
	st := &SuperToken{
		ID:                   uuid.NewV4(),
		IssuedAt:             now,
		NotBefore:            now,
		Issuer:               config.Get().IssuerURL,
		Subject:              issuerUtils.CombineSubIss(oidcSub, oidcIss),
		Audience:             config.Get().IssuerURL,
		OIDCIssuer:           oidcIss,
		OIDCSubject:          oidcSub,
		Capabilities:         c,
		SubtokenCapabilities: sc,
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	}
	if len(r) > 0 {
		st.Restrictions = r
		exp := r.GetExpires()
		if exp != 0 {
			st.ExpiresAt = exp
		}
		nbf := r.GetNotBefore()
		if nbf != 0 && nbf > now {
			st.NotBefore = nbf
		}
	}
	return st
}

// ExpiresIn returns the amount of seconds in which this token expires
func (st *SuperToken) ExpiresIn() uint64 {
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	now := time.Now().Unix()
	expAt := st.ExpiresAt
	if expAt > 0 && expAt > now {
Gabriel Zachmann's avatar
Gabriel Zachmann committed
		return uint64(expAt - now)
	}
	return 0
}

// Valid checks if this SuperToken is valid
Gabriel Zachmann's avatar
Gabriel Zachmann committed
func (st *SuperToken) Valid() error {
	standardClaims := jwt.StandardClaims{
		Audience:  st.Audience,
		ExpiresAt: st.ExpiresAt,
		Id:        st.ID.String(),
		IssuedAt:  st.IssuedAt,
		Issuer:    st.Issuer,
		NotBefore: st.NotBefore,
		Subject:   st.Subject,
	}
	if err := standardClaims.Valid(); err != nil {
		return err
	}
	if ok := standardClaims.VerifyIssuer(config.Get().IssuerURL, true); !ok {
		return fmt.Errorf("invalid issuer")
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	}
	if ok := standardClaims.VerifyAudience(config.Get().IssuerURL, true); !ok {
		return fmt.Errorf("invalid Audience")
	}
	if ok := st.verifyID(); !ok {
		return fmt.Errorf("invalid id")
	}
	if ok := st.verifySubject(); !ok {
		return fmt.Errorf("invalid subject")
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	}
	return nil
}

// ToSuperTokenResponse returns a SuperTokenResponse for this token. It requires that jwt is set or that the jwt is passed as argument; if not passed as argument toJWT must have been called earlier on this token to set jwt. This is always the case, if the token has been stored.
func (st *SuperToken) toSuperTokenResponse(jwt string) response.SuperTokenResponse {
	res := st.toTokenResponse()
	res.SuperToken = jwt
	res.SuperTokenType = model.ResponseTypeToken
	return res
}

func (st *SuperToken) toShortSuperTokenResponse(jwt string) (response.SuperTokenResponse, error) {
	shortToken, err := transfercoderepo.NewShortToken(jwt)
	if err != nil {
		return response.SuperTokenResponse{}, err
	}
	if err = shortToken.Store(nil); err != nil {
		return response.SuperTokenResponse{}, err
	}
	res := st.toTokenResponse()
	res.SuperToken = shortToken.String()
	res.SuperTokenType = model.ResponseTypeShortToken
	return res, nil
}

func (st *SuperToken) toTokenResponse() response.SuperTokenResponse {
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	return response.SuperTokenResponse{
		ExpiresIn:            st.ExpiresIn(),
		Restrictions:         st.Restrictions,
		Capabilities:         st.Capabilities,
		SubtokenCapabilities: st.SubtokenCapabilities,
// CreateTransferCode creates a transfer code for the passed super token
func CreateTransferCode(stid uuid.UUID, jwt string, newST bool, responseType model.ResponseType, clientMetaData model.ClientMetaData) (string, uint64, error) {
	transferCode, err := transfercoderepo.NewTransferCode(jwt, newST, responseType)
	if err != nil {
		return "", 0, err
	}
	err = db.Transact(func(tx *sqlx.Tx) error {
		if err = transferCode.Store(tx); err != nil {
			return err
		}
		return eventService.LogEvent(tx, &event.Event{
			Type:    event.STEventTransferCodeCreated,
			Comment: fmt.Sprintf("token type: %s", responseType.String()),
		}, stid, clientMetaData)
	})
	expiresIn := uint64(config.Get().Features.Polling.PollingCodeExpiresAfter)
	return transferCode.String(), expiresIn, err
// ToTokenResponse creates a SuperTokenResponse for this SuperToken according to the passed model.ResponseType
func (st *SuperToken) ToTokenResponse(responseType model.ResponseType, networkData model.ClientMetaData, jwt string) (response.SuperTokenResponse, error) {
	if len(jwt) == 0 {
		jwt = st.jwt
	}
	switch responseType {
	case model.ResponseTypeShortToken:
		if config.Get().Features.ShortTokens.Enabled {
			return st.toShortSuperTokenResponse(jwt)
		}
	case model.ResponseTypeTransferCode:
		transferCode, expiresIn, err := CreateTransferCode(st.ID, jwt, true, model.ResponseTypeToken, networkData)
		res := st.toTokenResponse()
		res.TransferCode = transferCode
		res.SuperTokenType = model.ResponseTypeTransferCode
		res.ExpiresIn = expiresIn
		return res, err
	}
	return st.toSuperTokenResponse(jwt), nil
}

// ToJWT returns the SuperToken as JWT
func (st *SuperToken) ToJWT() (string, error) {
Gabriel Zachmann's avatar
Gabriel Zachmann committed
	if st.jwt != "" {
		return st.jwt, nil
	}
	var err error
	st.jwt, err = jwt.NewWithClaims(jwt.GetSigningMethod(config.Get().Signing.Alg), st).SignedString(jws.GetPrivateKey())
	return st.jwt, err
Gabriel Zachmann's avatar
Gabriel Zachmann committed
}

// Value implements the driver.Valuer interface.
func (st *SuperToken) Value() (driver.Value, error) {
Gabriel Zachmann's avatar
Gabriel Zachmann committed
}

// Scan implements the sql.Scanner interface.
func (st *SuperToken) Scan(src interface{}) error {
	tmp, err := ParseJWT(src.(string))
	if err != nil {
		return err
	}
	*st = *tmp
	return nil
}

// ParseJWT parses a token string into a SuperToken
Gabriel Zachmann's avatar
Gabriel Zachmann committed
func ParseJWT(token string) (*SuperToken, error) {
	tok, err := jwt.ParseWithClaims(token, &SuperToken{}, func(t *jwt.Token) (interface{}, error) {
		return jws.GetPublicKey(), nil
	})
	if err != nil {
		return nil, err
	}

	if st, ok := tok.Claims.(*SuperToken); ok && tok.Valid {
		return st, nil
	}
	return nil, fmt.Errorf("token not valid")
Gabriel Zachmann's avatar
Gabriel Zachmann committed
}