package server import ( "errors" "io/ioutil" "net/url" "regexp" "strings" "time" "git.autistici.org/id/go-sso" ) var ( // ErrBadService means the service does not follow the specification. ErrBadService = errors.New("bad service") // ErrBadDestinationScheme is returned when the destination // scheme isn't HTTPS. ErrBadDestinationScheme = errors.New("bad destination scheme") // ErrUnauthorizedService means that the target service has // not been whitelisted. ErrUnauthorizedService = errors.New("unauthorized service") // ErrUnauthorizedDestination means that the destination is // not part of the service. ErrUnauthorizedDestination = errors.New("unauthorized destination") // ErrUnauthorized can be returned by the Exchange method if // group ACLs do not match the current ticket. ErrUnauthorized = errors.New("unauthorized") serviceRx = regexp.MustCompile(`^(?:(?:[a-z0-9][-a-z0-9]*\.)+[a-z]{2,4}|localhost)(?::[0-9]{2,5})?(?:/.*)?/$`) ) // LoginService provides the business logic for the SSO server, // offering the Authorize and Exchange methods. type LoginService struct { signer sso.Signer validator sso.Validator config *Config } func newSignerFromConfig(config *Config) (sso.Signer, error) { data, err := ioutil.ReadFile(config.SecretKeyFile) if err != nil { return nil, err } return sso.NewSigner(data) } func newValidatorFromConfig(config *Config) (sso.Validator, error) { data, err := ioutil.ReadFile(config.PublicKeyFile) if err != nil { return nil, err } return sso.NewValidator(data, config.Domain) } // NewLoginService returns a new LoginService with the specified configuration. func NewLoginService(config *Config) (*LoginService, error) { signer, err := newSignerFromConfig(config) if err != nil { return nil, err } validator, err := newValidatorFromConfig(config) if err != nil { return nil, err } return &LoginService{ config: config, signer: signer, validator: validator, }, nil } // Authorize a user to access a service by generating a token for // it. Note that the user must already be successfully identified by // some other means (e.g. passing a login form, etc). func (s *LoginService) Authorize(username, service, destination, nonce string, groups []string) (string, error) { if err := s.validateServiceAccess(service, destination); err != nil { return "", err } tkt := sso.NewTicket(username, service, s.config.Domain, nonce, groups, s.config.getServiceTTL(service)) return s.signer.Sign(tkt) } // Exchange a token for a new one scoped to a different service. func (s *LoginService) Exchange(curToken, curService, curNonce, newService, nonce string) (string, error) { // Check that the current token is valid for the current service. curTkt, err := s.validator.Validate(curToken, curNonce, curService, nil) if err != nil { return "", err } // Verify that the exchange transition is explicitly allowed. if err := s.validateServiceExchange(curService, newService); err != nil { return "", err } // The new ticket will not be valid any longer than the // original one, or for a maximum of whatever service-specific // TTL we have configured, whichever comes first. ttlLeft := time.Until(curTkt.Expires) if svcTTL := s.config.getServiceTTL(newService); svcTTL < ttlLeft { ttlLeft = svcTTL } // Generate a new signed ticket for the new service. Groups // in the original ticket are passed identically to the newly // generated ticket. tkt := sso.NewTicket(curTkt.User, newService, s.config.Domain, nonce, curTkt.Groups, ttlLeft) return s.signer.Sign(tkt) } func (s *LoginService) validateServiceAccess(service, destination string) error { // Check if the service is well-formed and allowed. if !serviceRx.MatchString(service) { return ErrBadService } if !s.config.isServiceAllowed(service) { return ErrUnauthorizedService } // Figure out what the service should be for the destination, // and compare it with service. destinationURL, err := url.Parse(destination) if err != nil { return err } // Generic sanity checks on the destination URL. if destinationURL.Scheme != "https" { return ErrBadDestinationScheme } destinationSvc := destinationURL.Host + destinationURL.Path if !strings.HasPrefix(destinationSvc, service) { return ErrUnauthorizedDestination } return nil } func (s *LoginService) validateServiceExchange(srcService, dstService string) error { // Check that both services are well-formed and allowed. if !serviceRx.MatchString(srcService) || !serviceRx.MatchString(dstService) { return ErrBadService } if !s.config.isServiceAllowed(srcService) || !s.config.isServiceAllowed(dstService) { return ErrUnauthorizedService } if !s.config.isExchangeAllowed(srcService, dstService) { return ErrUnauthorized } return nil }