build(deps): bump github.com/libregraph/lico from 0.66.0 to 0.67.0

Bumps [github.com/libregraph/lico](https://github.com/libregraph/lico) from 0.66.0 to 0.67.0.
- [Changelog](https://github.com/libregraph/lico/blob/master/CHANGELOG.md)
- [Commits](https://github.com/libregraph/lico/compare/v0.66.0...v0.67.0)

---
updated-dependencies:
- dependency-name: github.com/libregraph/lico
  dependency-version: 0.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-06-26 14:43:28 +00:00
committed by Ralf Haferkamp
parent eee040d066
commit 85be8e726b
33 changed files with 377 additions and 1339 deletions

3
go.mod
View File

@@ -52,7 +52,7 @@ require (
github.com/kovidgoyal/imaging v1.8.21
github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69
github.com/libregraph/idm v0.5.0
github.com/libregraph/lico v0.66.0
github.com/libregraph/lico v0.67.0
github.com/mna/pigeon v1.3.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/nats-io/nats-server/v2 v2.14.2
@@ -231,7 +231,6 @@ require (
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect

7
go.sum
View File

@@ -482,9 +482,8 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -773,8 +772,8 @@ github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLO
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/libregraph/idm v0.5.0 h1:tDMwKbAOZzdeDYMxVlY5PbSqRKO7dbAW9KT42A51WSk=
github.com/libregraph/idm v0.5.0/go.mod h1:BGMwIQ/6orJSPVzJ1x6kgG2JyG9GY05YFmbsnaD80k0=
github.com/libregraph/lico v0.66.0 h1:7T6fD1YF0Ep9n0g4KN6dvWHTlDC3awrQpgsP5GdYCF4=
github.com/libregraph/lico v0.66.0/go.mod h1:QI7NfmAkAWQ2y97iVfLv10S8tcvPQjc630uyfHGjIOw=
github.com/libregraph/lico v0.67.0 h1:qsVjDVHyEGZxrKU9PEsdim7aKnEz5CVtG3VAbZ77hFU=
github.com/libregraph/lico v0.67.0/go.mod h1:kvfaHXyIlualEubTKszOizY65oGasC1oHmFpfQt4ugQ=
github.com/libregraph/oidc-go v1.1.0 h1:RyudjL3UyQblqeBQI06W53PniWobqODeeyAy6v/HumA=
github.com/libregraph/oidc-go v1.1.0/go.mod h1:qW9ubcXvZrfbbWZBaLMuk7bt5qAUMYyt9/NtXQt07Cw=
github.com/linode/linodego v0.25.3/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE=

View File

@@ -1,15 +0,0 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# binary bundle generated by go-fuzz
uuid-fuzz.zip

20
vendor/github.com/gofrs/uuid/LICENSE generated vendored
View File

@@ -1,20 +0,0 @@
Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,117 +0,0 @@
# UUID
[![License](https://img.shields.io/github/license/gofrs/uuid.svg)](https://github.com/gofrs/uuid/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/gofrs/uuid.svg?branch=master)](https://travis-ci.org/gofrs/uuid)
[![GoDoc](http://godoc.org/github.com/gofrs/uuid?status.svg)](http://godoc.org/github.com/gofrs/uuid)
[![Coverage Status](https://codecov.io/gh/gofrs/uuid/branch/master/graphs/badge.svg?branch=master)](https://codecov.io/gh/gofrs/uuid/)
[![Go Report Card](https://goreportcard.com/badge/github.com/gofrs/uuid)](https://goreportcard.com/report/github.com/gofrs/uuid)
Package uuid provides a pure Go implementation of Universally Unique Identifiers
(UUID) variant as defined in RFC-4122. This package supports both the creation
and parsing of UUIDs in different formats.
This package supports the following UUID versions:
* Version 1, based on timestamp and MAC address (RFC-4122)
* Version 3, based on MD5 hashing of a named value (RFC-4122)
* Version 4, based on random numbers (RFC-4122)
* Version 5, based on SHA-1 hashing of a named value (RFC-4122)
This package also supports experimental Universally Unique Identifier implementations based on a
[draft RFC](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html) that updates RFC-4122
* Version 6, a k-sortable id based on timestamp, and field-compatible with v1 (draft-peabody-dispatch-new-uuid-format, RFC-4122)
* Version 7, a k-sortable id based on timestamp (draft-peabody-dispatch-new-uuid-format, RFC-4122)
The v6 and v7 IDs are **not** considered a part of the stable API, and may be subject to behavior or API changes as part of minor releases
to this package. They will be updated as the draft RFC changes, and will become stable if and when the draft RFC is accepted.
## Project History
This project was originally forked from the
[github.com/satori/go.uuid](https://github.com/satori/go.uuid) repository after
it appeared to be no longer maintained, while exhibiting [critical
flaws](https://github.com/satori/go.uuid/issues/73). We have decided to take
over this project to ensure it receives regular maintenance for the benefit of
the larger Go community.
We'd like to thank Maxim Bublis for his hard work on the original iteration of
the package.
## License
This source code of this package is released under the MIT License. Please see
the [LICENSE](https://github.com/gofrs/uuid/blob/master/LICENSE) for the full
content of the license.
## Recommended Package Version
We recommend using v2.0.0+ of this package, as versions prior to 2.0.0 were
created before our fork of the original package and have some known
deficiencies.
## Installation
It is recommended to use a package manager like `dep` that understands tagged
releases of a package, as well as semantic versioning.
If you are unable to make use of a dependency manager with your project, you can
use the `go get` command to download it directly:
```Shell
$ go get github.com/gofrs/uuid
```
## Requirements
Due to subtests not being supported in older versions of Go, this package is
only regularly tested against Go 1.7+. This package may work perfectly fine with
Go 1.2+, but support for these older versions is not actively maintained.
## Go 1.11 Modules
As of v3.2.0, this repository no longer adopts Go modules, and v3.2.0 no longer has a `go.mod` file. As a result, v3.2.0 also drops support for the `github.com/gofrs/uuid/v3` import path. Only module-based consumers are impacted. With the v3.2.0 release, _all_ gofrs/uuid consumers should use the `github.com/gofrs/uuid` import path.
An existing module-based consumer will continue to be able to build using the `github.com/gofrs/uuid/v3` import path using any valid consumer `go.mod` that worked prior to the publishing of v3.2.0, but any module-based consumer should start using the `github.com/gofrs/uuid` import path when possible and _must_ use the `github.com/gofrs/uuid` import path prior to upgrading to v3.2.0.
Please refer to [Issue #61](https://github.com/gofrs/uuid/issues/61) and [Issue #66](https://github.com/gofrs/uuid/issues/66) for more details.
## Usage
Here is a quick overview of how to use this package. For more detailed
documentation, please see the [GoDoc Page](http://godoc.org/github.com/gofrs/uuid).
```go
package main
import (
"log"
"github.com/gofrs/uuid"
)
// Create a Version 4 UUID, panicking on error.
// Use this form to initialize package-level variables.
var u1 = uuid.Must(uuid.NewV4())
func main() {
// Create a Version 4 UUID.
u2, err := uuid.NewV4()
if err != nil {
log.Fatalf("failed to generate UUID: %v", err)
}
log.Printf("generated Version 4 UUID %v", u2)
// Parse a UUID from a string.
s := "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
u3, err := uuid.FromString(s)
if err != nil {
log.Fatalf("failed to parse UUID %q: %v", s, err)
}
log.Printf("successfully parsed UUID %v", u3)
}
```
## References
* [RFC-4122](https://tools.ietf.org/html/rfc4122)
* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01)
* [New UUID Formats RFC Draft (Peabody) Rev 04](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#)

234
vendor/github.com/gofrs/uuid/codec.go generated vendored
View File

@@ -1,234 +0,0 @@
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package uuid
import (
"errors"
"fmt"
)
// FromBytes returns a UUID generated from the raw byte slice input.
// It will return an error if the slice isn't 16 bytes long.
func FromBytes(input []byte) (UUID, error) {
u := UUID{}
err := u.UnmarshalBinary(input)
return u, err
}
// FromBytesOrNil returns a UUID generated from the raw byte slice input.
// Same behavior as FromBytes(), but returns uuid.Nil instead of an error.
func FromBytesOrNil(input []byte) UUID {
uuid, err := FromBytes(input)
if err != nil {
return Nil
}
return uuid
}
var errInvalidFormat = errors.New("uuid: invalid UUID format")
func fromHexChar(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 255
}
// Parse parses the UUID stored in the string text. Parsing and supported
// formats are the same as UnmarshalText.
func (u *UUID) Parse(s string) error {
switch len(s) {
case 32: // hash
case 36: // canonical
case 34, 38:
if s[0] != '{' || s[len(s)-1] != '}' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
}
s = s[1 : len(s)-1]
case 41, 45:
if s[:9] != "urn:uuid:" {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s[:9])
}
s = s[9:]
default:
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(s), s)
}
// canonical
if len(s) == 36 {
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
}
for i, x := range [16]byte{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34,
} {
v1 := fromHexChar(s[x])
v2 := fromHexChar(s[x+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i] = (v1 << 4) | v2
}
return nil
}
// hash like
for i := 0; i < 32; i += 2 {
v1 := fromHexChar(s[i])
v2 := fromHexChar(s[i+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
return nil
}
// FromString returns a UUID parsed from the input string.
// Input is expected in a form accepted by UnmarshalText.
func FromString(text string) (UUID, error) {
var u UUID
err := u.Parse(text)
return u, err
}
// FromStringOrNil returns a UUID parsed from the input string.
// Same behavior as FromString(), but returns uuid.Nil instead of an error.
func FromStringOrNil(input string) UUID {
uuid, err := FromString(input)
if err != nil {
return Nil
}
return uuid
}
// MarshalText implements the encoding.TextMarshaler interface.
// The encoding is the same as returned by the String() method.
func (u UUID) MarshalText() ([]byte, error) {
var buf [36]byte
encodeCanonical(buf[:], u)
return buf[:], nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// Following formats are supported:
//
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
// "6ba7b8109dad11d180b400c04fd430c8"
// "{6ba7b8109dad11d180b400c04fd430c8}",
// "urn:uuid:6ba7b8109dad11d180b400c04fd430c8"
//
// ABNF for supported UUID text representation follows:
//
// URN := 'urn'
// UUID-NID := 'uuid'
//
// hexdig := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' |
// 'a' | 'b' | 'c' | 'd' | 'e' | 'f' |
// 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
//
// hexoct := hexdig hexdig
// 2hexoct := hexoct hexoct
// 4hexoct := 2hexoct 2hexoct
// 6hexoct := 4hexoct 2hexoct
// 12hexoct := 6hexoct 6hexoct
//
// hashlike := 12hexoct
// canonical := 4hexoct '-' 2hexoct '-' 2hexoct '-' 6hexoct
//
// plain := canonical | hashlike
// uuid := canonical | hashlike | braced | urn
//
// braced := '{' plain '}' | '{' hashlike '}'
// urn := URN ':' UUID-NID ':' plain
func (u *UUID) UnmarshalText(b []byte) error {
switch len(b) {
case 32: // hash
case 36: // canonical
case 34, 38:
if b[0] != '{' || b[len(b)-1] != '}' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
}
b = b[1 : len(b)-1]
case 41, 45:
if string(b[:9]) != "urn:uuid:" {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b[:9])
}
b = b[9:]
default:
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(b), b)
}
if len(b) == 36 {
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
}
for i, x := range [16]byte{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34,
} {
v1 := fromHexChar(b[x])
v2 := fromHexChar(b[x+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i] = (v1 << 4) | v2
}
return nil
}
for i := 0; i < 32; i += 2 {
v1 := fromHexChar(b[i])
v2 := fromHexChar(b[i+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
return nil
}
// MarshalBinary implements the encoding.BinaryMarshaler interface.
func (u UUID) MarshalBinary() ([]byte, error) {
return u.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
// It will return an error if the slice isn't 16 bytes long.
func (u *UUID) UnmarshalBinary(data []byte) error {
if len(data) != Size {
return fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data))
}
copy(u[:], data)
return nil
}

48
vendor/github.com/gofrs/uuid/fuzz.go generated vendored
View File

@@ -1,48 +0,0 @@
// Copyright (c) 2018 Andrei Tudor Călin <mail@acln.ro>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//go:build gofuzz
// +build gofuzz
package uuid
// Fuzz implements a simple fuzz test for FromString / UnmarshalText.
//
// To run:
//
// $ go get github.com/dvyukov/go-fuzz/...
// $ cd $GOPATH/src/github.com/gofrs/uuid
// $ go-fuzz-build github.com/gofrs/uuid
// $ go-fuzz -bin=uuid-fuzz.zip -workdir=./testdata
//
// If you make significant changes to FromString / UnmarshalText and add
// new cases to fromStringTests (in codec_test.go), please run
//
// $ go test -seed_fuzz_corpus
//
// to seed the corpus with the new interesting inputs, then run the fuzzer.
func Fuzz(data []byte) int {
_, err := FromString(string(data))
if err != nil {
return 0
}
return 1
}

View File

@@ -1,456 +0,0 @@
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package uuid
import (
"crypto/md5"
"crypto/rand"
"crypto/sha1"
"encoding/binary"
"fmt"
"hash"
"io"
"net"
"sync"
"time"
)
// Difference in 100-nanosecond intervals between
// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970).
const epochStart = 122192928000000000
// EpochFunc is the function type used to provide the current time.
type EpochFunc func() time.Time
// HWAddrFunc is the function type used to provide hardware (MAC) addresses.
type HWAddrFunc func() (net.HardwareAddr, error)
// DefaultGenerator is the default UUID Generator used by this package.
var DefaultGenerator Generator = NewGen()
// NewV1 returns a UUID based on the current timestamp and MAC address.
func NewV1() (UUID, error) {
return DefaultGenerator.NewV1()
}
// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name.
func NewV3(ns UUID, name string) UUID {
return DefaultGenerator.NewV3(ns, name)
}
// NewV4 returns a randomly generated UUID.
func NewV4() (UUID, error) {
return DefaultGenerator.NewV4()
}
// NewV5 returns a UUID based on SHA-1 hash of the namespace UUID and name.
func NewV5(ns UUID, name string) UUID {
return DefaultGenerator.NewV5(ns, name)
}
// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
// order being adjusted to allow the UUID to be k-sortable.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func NewV6() (UUID, error) {
return DefaultGenerator.NewV6()
}
// NewV7 returns a k-sortable UUID based on the current millisecond precision
// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter.
//
// This is implemented based on revision 04 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func NewV7() (UUID, error) {
return DefaultGenerator.NewV7()
}
// Generator provides an interface for generating UUIDs.
type Generator interface {
NewV1() (UUID, error)
NewV3(ns UUID, name string) UUID
NewV4() (UUID, error)
NewV5(ns UUID, name string) UUID
NewV6() (UUID, error)
NewV7() (UUID, error)
}
// Gen is a reference UUID generator based on the specifications laid out in
// RFC-4122 and DCE 1.1: Authentication and Security Services. This type
// satisfies the Generator interface as defined in this package.
//
// For consumers who are generating V1 UUIDs, but don't want to expose the MAC
// address of the node generating the UUIDs, the NewGenWithHWAF() function has been
// provided as a convenience. See the function's documentation for more info.
//
// The authors of this package do not feel that the majority of users will need
// to obfuscate their MAC address, and so we recommend using NewGen() to create
// a new generator.
type Gen struct {
clockSequenceOnce sync.Once
hardwareAddrOnce sync.Once
storageMutex sync.Mutex
rand io.Reader
epochFunc EpochFunc
hwAddrFunc HWAddrFunc
lastTime uint64
clockSequence uint16
hardwareAddr [6]byte
}
// GenOption is a function type that can be used to configure a Gen generator.
type GenOption func(*Gen)
// interface check -- build will fail if *Gen doesn't satisfy Generator
var _ Generator = (*Gen)(nil)
// NewGen returns a new instance of Gen with some default values set. Most
// people should use this.
func NewGen() *Gen {
return NewGenWithHWAF(defaultHWAddrFunc)
}
// NewGenWithHWAF builds a new UUID generator with the HWAddrFunc provided. Most
// consumers should use NewGen() instead.
//
// This is used so that consumers can generate their own MAC addresses, for use
// in the generated UUIDs, if there is some concern about exposing the physical
// address of the machine generating the UUID.
//
// The Gen generator will only invoke the HWAddrFunc once, and cache that MAC
// address for all the future UUIDs generated by it. If you'd like to switch the
// MAC address being used, you'll need to create a new generator using this
// function.
func NewGenWithHWAF(hwaf HWAddrFunc) *Gen {
return NewGenWithOptions(WithHWAddrFunc(hwaf))
}
// NewGenWithOptions returns a new instance of Gen with the options provided.
// Most people should use NewGen() or NewGenWithHWAF() instead.
//
// To customize the generator, you can pass in one or more GenOption functions.
// For example:
//
// gen := NewGenWithOptions(
// WithHWAddrFunc(myHWAddrFunc),
// WithEpochFunc(myEpochFunc),
// WithRandomReader(myRandomReader),
// )
//
// NewGenWithOptions(WithHWAddrFunc(myHWAddrFunc)) is equivalent to calling
// NewGenWithHWAF(myHWAddrFunc)
// NewGenWithOptions() is equivalent to calling NewGen()
func NewGenWithOptions(opts ...GenOption) *Gen {
gen := &Gen{
epochFunc: time.Now,
hwAddrFunc: defaultHWAddrFunc,
rand: rand.Reader,
}
for _, opt := range opts {
opt(gen)
}
return gen
}
// WithHWAddrFunc is a GenOption that allows you to provide your own HWAddrFunc
// function.
// When this option is nil, the defaultHWAddrFunc is used.
func WithHWAddrFunc(hwaf HWAddrFunc) GenOption {
return func(gen *Gen) {
if hwaf == nil {
hwaf = defaultHWAddrFunc
}
gen.hwAddrFunc = hwaf
}
}
// WithEpochFunc is a GenOption that allows you to provide your own EpochFunc
// function.
// When this option is nil, time.Now is used.
func WithEpochFunc(epochf EpochFunc) GenOption {
return func(gen *Gen) {
if epochf == nil {
epochf = time.Now
}
gen.epochFunc = epochf
}
}
// WithRandomReader is a GenOption that allows you to provide your own random
// reader.
// When this option is nil, the default rand.Reader is used.
func WithRandomReader(reader io.Reader) GenOption {
return func(gen *Gen) {
if reader == nil {
reader = rand.Reader
}
gen.rand = reader
}
}
// NewV1 returns a UUID based on the current timestamp and MAC address.
func (g *Gen) NewV1() (UUID, error) {
u := UUID{}
timeNow, clockSeq, err := g.getClockSequence(false)
if err != nil {
return Nil, err
}
binary.BigEndian.PutUint32(u[0:], uint32(timeNow))
binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32))
binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48))
binary.BigEndian.PutUint16(u[8:], clockSeq)
hardwareAddr, err := g.getHardwareAddr()
if err != nil {
return Nil, err
}
copy(u[10:], hardwareAddr)
u.SetVersion(V1)
u.SetVariant(VariantRFC4122)
return u, nil
}
// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name.
func (g *Gen) NewV3(ns UUID, name string) UUID {
u := newFromHash(md5.New(), ns, name)
u.SetVersion(V3)
u.SetVariant(VariantRFC4122)
return u
}
// NewV4 returns a randomly generated UUID.
func (g *Gen) NewV4() (UUID, error) {
u := UUID{}
if _, err := io.ReadFull(g.rand, u[:]); err != nil {
return Nil, err
}
u.SetVersion(V4)
u.SetVariant(VariantRFC4122)
return u, nil
}
// NewV5 returns a UUID based on SHA-1 hash of the namespace UUID and name.
func (g *Gen) NewV5(ns UUID, name string) UUID {
u := newFromHash(sha1.New(), ns, name)
u.SetVersion(V5)
u.SetVariant(VariantRFC4122)
return u
}
// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
// order being adjusted to allow the UUID to be k-sortable.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func (g *Gen) NewV6() (UUID, error) {
var u UUID
if _, err := io.ReadFull(g.rand, u[10:]); err != nil {
return Nil, err
}
timeNow, clockSeq, err := g.getClockSequence(false)
if err != nil {
return Nil, err
}
binary.BigEndian.PutUint32(u[0:], uint32(timeNow>>28)) // set time_high
binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>12)) // set time_mid
binary.BigEndian.PutUint16(u[6:], uint16(timeNow&0xfff)) // set time_low (minus four version bits)
binary.BigEndian.PutUint16(u[8:], clockSeq&0x3fff) // set clk_seq_hi_res (minus two variant bits)
u.SetVersion(V6)
u.SetVariant(VariantRFC4122)
return u, nil
}
// getClockSequence returns the epoch and clock sequence for V1,V6 and V7 UUIDs.
//
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of 100-
//
// nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian reform to the Christian calendar).
func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {
var err error
g.clockSequenceOnce.Do(func() {
buf := make([]byte, 2)
if _, err = io.ReadFull(g.rand, buf); err != nil {
return
}
g.clockSequence = binary.BigEndian.Uint16(buf)
})
if err != nil {
return 0, 0, err
}
g.storageMutex.Lock()
defer g.storageMutex.Unlock()
var timeNow uint64
if useUnixTSMs {
timeNow = uint64(g.epochFunc().UnixMilli())
} else {
timeNow = g.getEpoch()
}
// Clock didn't change since last UUID generation.
// Should increase clock sequence.
if timeNow <= g.lastTime {
g.clockSequence++
}
g.lastTime = timeNow
return timeNow, g.clockSequence, nil
}
// NewV7 returns a k-sortable UUID based on the current millisecond precision
// UNIX epoch and 74 bits of pseudorandom data.
//
// This is implemented based on revision 04 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func (g *Gen) NewV7() (UUID, error) {
var u UUID
/* https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
ms, clockSeq, err := g.getClockSequence(true)
if err != nil {
return Nil, err
}
//UUIDv7 features a 48 bit timestamp. First 32bit (4bytes) represents seconds since 1970, followed by 2 bytes for the ms granularity.
u[0] = byte(ms >> 40) //1-6 bytes: big-endian unsigned number of Unix epoch timestamp
u[1] = byte(ms >> 32)
u[2] = byte(ms >> 24)
u[3] = byte(ms >> 16)
u[4] = byte(ms >> 8)
u[5] = byte(ms)
//support batching by using a monotonic pseudo-random sequence
//The 6th byte contains the version and partially rand_a data.
//We will lose the most significant bites from the clockSeq (with SetVersion), but it is ok, we need the least significant that contains the counter to ensure the monotonic property
binary.BigEndian.PutUint16(u[6:8], clockSeq) // set rand_a with clock seq which is random and monotonic
//override first 4bits of u[6].
u.SetVersion(V7)
//set rand_b 64bits of pseudo-random bits (first 2 will be overridden)
if _, err = io.ReadFull(g.rand, u[8:16]); err != nil {
return Nil, err
}
//override first 2 bits of byte[8] for the variant
u.SetVariant(VariantRFC4122)
return u, nil
}
// Returns the hardware address.
func (g *Gen) getHardwareAddr() ([]byte, error) {
var err error
g.hardwareAddrOnce.Do(func() {
var hwAddr net.HardwareAddr
if hwAddr, err = g.hwAddrFunc(); err == nil {
copy(g.hardwareAddr[:], hwAddr)
return
}
// Initialize hardwareAddr randomly in case
// of real network interfaces absence.
if _, err = io.ReadFull(g.rand, g.hardwareAddr[:]); err != nil {
return
}
// Set multicast bit as recommended by RFC-4122
g.hardwareAddr[0] |= 0x01
})
if err != nil {
return []byte{}, err
}
return g.hardwareAddr[:], nil
}
// Returns the difference between UUID epoch (October 15, 1582)
// and current time in 100-nanosecond intervals.
func (g *Gen) getEpoch() uint64 {
return epochStart + uint64(g.epochFunc().UnixNano()/100)
}
// Returns the UUID based on the hashing of the namespace UUID and name.
func newFromHash(h hash.Hash, ns UUID, name string) UUID {
u := UUID{}
h.Write(ns[:])
h.Write([]byte(name))
copy(u[:], h.Sum(nil))
return u
}
var netInterfaces = net.Interfaces
// Returns the hardware address.
func defaultHWAddrFunc() (net.HardwareAddr, error) {
ifaces, err := netInterfaces()
if err != nil {
return []byte{}, err
}
for _, iface := range ifaces {
if len(iface.HardwareAddr) >= 6 {
return iface.HardwareAddr, nil
}
}
return []byte{}, fmt.Errorf("uuid: no HW address found")
}

116
vendor/github.com/gofrs/uuid/sql.go generated vendored
View File

@@ -1,116 +0,0 @@
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package uuid
import (
"database/sql"
"database/sql/driver"
"fmt"
)
var _ driver.Valuer = UUID{}
var _ sql.Scanner = (*UUID)(nil)
// Value implements the driver.Valuer interface.
func (u UUID) Value() (driver.Value, error) {
return u.String(), nil
}
// Scan implements the sql.Scanner interface.
// A 16-byte slice will be handled by UnmarshalBinary, while
// a longer byte slice or a string will be handled by UnmarshalText.
func (u *UUID) Scan(src interface{}) error {
switch src := src.(type) {
case UUID: // support gorm convert from UUID to NullUUID
*u = src
return nil
case []byte:
if len(src) == Size {
return u.UnmarshalBinary(src)
}
return u.UnmarshalText(src)
case string:
uu, err := FromString(src)
*u = uu
return err
}
return fmt.Errorf("uuid: cannot convert %T to UUID", src)
}
// NullUUID can be used with the standard sql package to represent a
// UUID value that can be NULL in the database.
type NullUUID struct {
UUID UUID
Valid bool
}
// Value implements the driver.Valuer interface.
func (u NullUUID) Value() (driver.Value, error) {
if !u.Valid {
return nil, nil
}
// Delegate to UUID Value function
return u.UUID.Value()
}
// Scan implements the sql.Scanner interface.
func (u *NullUUID) Scan(src interface{}) error {
if src == nil {
u.UUID, u.Valid = Nil, false
return nil
}
// Delegate to UUID Scan function
u.Valid = true
return u.UUID.Scan(src)
}
var nullJSON = []byte("null")
// MarshalJSON marshals the NullUUID as null or the nested UUID
func (u NullUUID) MarshalJSON() ([]byte, error) {
if !u.Valid {
return nullJSON, nil
}
var buf [38]byte
buf[0] = '"'
encodeCanonical(buf[1:37], u.UUID)
buf[37] = '"'
return buf[:], nil
}
// UnmarshalJSON unmarshals a NullUUID
func (u *NullUUID) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
u.UUID, u.Valid = Nil, false
return nil
}
if n := len(b); n >= 2 && b[0] == '"' {
b = b[1 : n-1]
}
err := u.UUID.UnmarshalText(b)
u.Valid = (err == nil)
return err
}

285
vendor/github.com/gofrs/uuid/uuid.go generated vendored
View File

@@ -1,285 +0,0 @@
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Package uuid provides implementations of the Universally Unique Identifier
// (UUID), as specified in RFC-4122 and the Peabody RFC Draft (revision 03).
//
// RFC-4122[1] provides the specification for versions 1, 3, 4, and 5. The
// Peabody UUID RFC Draft[2] provides the specification for the new k-sortable
// UUIDs, versions 6 and 7.
//
// DCE 1.1[3] provides the specification for version 2, but version 2 support
// was removed from this package in v4 due to some concerns with the
// specification itself. Reading the spec, it seems that it would result in
// generating UUIDs that aren't very unique. In having read the spec it seemed
// that our implementation did not meet the spec. It also seems to be at-odds
// with RFC 4122, meaning we would need quite a bit of special code to support
// it. Lastly, there were no Version 2 implementations that we could find to
// ensure we were understanding the specification correctly.
//
// [1] https://tools.ietf.org/html/rfc4122
// [2] https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03
// [3] http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01
package uuid
import (
"encoding/binary"
"encoding/hex"
"fmt"
"time"
)
// Size of a UUID in bytes.
const Size = 16
// UUID is an array type to represent the value of a UUID, as defined in RFC-4122.
type UUID [Size]byte
// UUID versions.
const (
_ byte = iota
V1 // Version 1 (date-time and MAC address)
_ // Version 2 (date-time and MAC address, DCE security version) [removed]
V3 // Version 3 (namespace name-based)
V4 // Version 4 (random)
V5 // Version 5 (namespace name-based)
V6 // Version 6 (k-sortable timestamp and random data, field-compatible with v1) [peabody draft]
V7 // Version 7 (k-sortable timestamp and random data) [peabody draft]
_ // Version 8 (k-sortable timestamp, meant for custom implementations) [peabody draft] [not implemented]
)
// UUID layout variants.
const (
VariantNCS byte = iota
VariantRFC4122
VariantMicrosoft
VariantFuture
)
// UUID DCE domains.
const (
DomainPerson = iota
DomainGroup
DomainOrg
)
// Timestamp is the count of 100-nanosecond intervals since 00:00:00.00,
// 15 October 1582 within a V1 UUID. This type has no meaning for other
// UUID versions since they don't have an embedded timestamp.
type Timestamp uint64
const _100nsPerSecond = 10000000
// Time returns the UTC time.Time representation of a Timestamp
func (t Timestamp) Time() (time.Time, error) {
secs := uint64(t) / _100nsPerSecond
nsecs := 100 * (uint64(t) % _100nsPerSecond)
return time.Unix(int64(secs)-(epochStart/_100nsPerSecond), int64(nsecs)), nil
}
// TimestampFromV1 returns the Timestamp embedded within a V1 UUID.
// Returns an error if the UUID is any version other than 1.
func TimestampFromV1(u UUID) (Timestamp, error) {
if u.Version() != 1 {
err := fmt.Errorf("uuid: %s is version %d, not version 1", u, u.Version())
return 0, err
}
low := binary.BigEndian.Uint32(u[0:4])
mid := binary.BigEndian.Uint16(u[4:6])
hi := binary.BigEndian.Uint16(u[6:8]) & 0xfff
return Timestamp(uint64(low) + (uint64(mid) << 32) + (uint64(hi) << 48)), nil
}
// TimestampFromV6 returns the Timestamp embedded within a V6 UUID. This
// function returns an error if the UUID is any version other than 6.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func TimestampFromV6(u UUID) (Timestamp, error) {
if u.Version() != 6 {
return 0, fmt.Errorf("uuid: %s is version %d, not version 6", u, u.Version())
}
hi := binary.BigEndian.Uint32(u[0:4])
mid := binary.BigEndian.Uint16(u[4:6])
low := binary.BigEndian.Uint16(u[6:8]) & 0xfff
return Timestamp(uint64(low) + (uint64(mid) << 12) + (uint64(hi) << 28)), nil
}
// Nil is the nil UUID, as specified in RFC-4122, that has all 128 bits set to
// zero.
var Nil = UUID{}
// Predefined namespace UUIDs.
var (
NamespaceDNS = Must(FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
NamespaceURL = Must(FromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
NamespaceOID = Must(FromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
NamespaceX500 = Must(FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
)
// IsNil returns if the UUID is equal to the nil UUID
func (u UUID) IsNil() bool {
return u == Nil
}
// Version returns the algorithm version used to generate the UUID.
func (u UUID) Version() byte {
return u[6] >> 4
}
// Variant returns the UUID layout variant.
func (u UUID) Variant() byte {
switch {
case (u[8] >> 7) == 0x00:
return VariantNCS
case (u[8] >> 6) == 0x02:
return VariantRFC4122
case (u[8] >> 5) == 0x06:
return VariantMicrosoft
case (u[8] >> 5) == 0x07:
fallthrough
default:
return VariantFuture
}
}
// Bytes returns a byte slice representation of the UUID.
func (u UUID) Bytes() []byte {
return u[:]
}
// encodeCanonical encodes the canonical RFC-4122 form of UUID u into the
// first 36 bytes dst.
func encodeCanonical(dst []byte, u UUID) {
const hextable = "0123456789abcdef"
dst[8] = '-'
dst[13] = '-'
dst[18] = '-'
dst[23] = '-'
for i, x := range [16]byte{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34,
} {
c := u[i]
dst[x] = hextable[c>>4]
dst[x+1] = hextable[c&0x0f]
}
}
// String returns a canonical RFC-4122 string representation of the UUID:
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
func (u UUID) String() string {
var buf [36]byte
encodeCanonical(buf[:], u)
return string(buf[:])
}
// Format implements fmt.Formatter for UUID values.
//
// The behavior is as follows:
// The 'x' and 'X' verbs output only the hex digits of the UUID, using a-f for 'x' and A-F for 'X'.
// The 'v', '+v', 's' and 'q' verbs return the canonical RFC-4122 string representation.
// The 'S' verb returns the RFC-4122 format, but with capital hex digits.
// The '#v' verb returns the "Go syntax" representation, which is a 16 byte array initializer.
// All other verbs not handled directly by the fmt package (like '%p') are unsupported and will return
// "%!verb(uuid.UUID=value)" as recommended by the fmt package.
func (u UUID) Format(f fmt.State, c rune) {
if c == 'v' && f.Flag('#') {
fmt.Fprintf(f, "%#v", [Size]byte(u))
return
}
switch c {
case 'x', 'X':
b := make([]byte, 32)
hex.Encode(b, u[:])
if c == 'X' {
toUpperHex(b)
}
_, _ = f.Write(b)
case 'v', 's', 'S':
b, _ := u.MarshalText()
if c == 'S' {
toUpperHex(b)
}
_, _ = f.Write(b)
case 'q':
b := make([]byte, 38)
b[0] = '"'
encodeCanonical(b[1:], u)
b[37] = '"'
_, _ = f.Write(b)
default:
// invalid/unsupported format verb
fmt.Fprintf(f, "%%!%c(uuid.UUID=%s)", c, u.String())
}
}
func toUpperHex(b []byte) {
for i, c := range b {
if 'a' <= c && c <= 'f' {
b[i] = c - ('a' - 'A')
}
}
}
// SetVersion sets the version bits.
func (u *UUID) SetVersion(v byte) {
u[6] = (u[6] & 0x0f) | (v << 4)
}
// SetVariant sets the variant bits.
func (u *UUID) SetVariant(v byte) {
switch v {
case VariantNCS:
u[8] = (u[8]&(0xff>>1) | (0x00 << 7))
case VariantRFC4122:
u[8] = (u[8]&(0xff>>2) | (0x02 << 6))
case VariantMicrosoft:
u[8] = (u[8]&(0xff>>3) | (0x06 << 5))
case VariantFuture:
fallthrough
default:
u[8] = (u[8]&(0xff>>3) | (0x07 << 5))
}
}
// Must is a helper that wraps a call to a function returning (UUID, error)
// and panics if the error is non-nil. It is intended for use in variable
// initializations such as
//
// var packageUUID = uuid.Must(uuid.FromString("123e4567-e89b-12d3-a456-426655440000"))
func Must(u UUID, err error) UUID {
if err != nil {
panic(err)
}
return u
}

View File

@@ -4,7 +4,31 @@
## v0.66.0 (2025-04-28)
## v0.67.0 (2026-03-18)
- Bump github.com/russellhaering/goxmldsig from 1.5.0 to 1.6.0
- Run npx update-browserslist-db@latest
- Add signed JWT auto sign-in flow (LibreGraph.SignedLoginOK)
- Fix Go formatting treewide
- Add per-client external authorize redirect URIs
- Rework banner logo height to use named sizes instead of pixels
- Add configurable banner logo height via --identifier-default-banner-logo-height
- Bump docs to match Go 1.24 requirement
- Bump github.com/prometheus/client_golang from 1.15.1 to 1.23.2
- Bump github.com/beevik/etree from 1.5.1 to 1.6.0
- Bump golang.org/x/crypto from 0.37.0 to 0.45.0
- chore: drop gofrs/uuid module usage and use google/uuid
- Update golangci-lint to version 2
- Bump golang.org/x/oauth2 from 0.8.0 to 0.31.0
- Bump golang.org/x/time from 0.3.0 to 0.13.0
- Bump form-data from 4.0.0 to 4.0.4 in /identifier
- Bump golang.org/x/oauth2 from 0.8.0 to 0.27.0
- Fix typos an add API section to README
- Fix form submission handler regression introduced in class-to-functional conversion
- Replace konnect-identifier-api-v1 with comprehensive LibreGraph Connect OpenAPI spec
## v0.66.0 (2025-05-12)
- Remove built-in survey client from the licod runner
- Bump caniuse-lite to latest

View File

@@ -40,7 +40,7 @@ combine the implementation details.
## Build dependencies
Make sure you have Go 1.18 or later installed. This project uses Go Modules.
Make sure you have Go 1.24 or later installed. This project uses Go Modules.
Lico also includes a modern web app which requires a couple of additional
build dependencies which are furthermore also assumed to be in your $PATH.
@@ -51,7 +51,7 @@ build dependencies which are furthermore also assumed to be in your $PATH.
To build Lico, a `Makefile` is provided, which requires [make](https://www.gnu.org/software/make/manual/make.html).
When building, third party dependencies will tried to be fetched from the Internet
When building, third party dependencies will be fetched from the Internet
if not there already.
## Building from source
@@ -99,7 +99,7 @@ this, Lico will generate a random key on startup.
To run a functional OpenID Connect provider, an issuer identifier is required.
The `iss` is a full qualified https:// URI pointing to the web server which
serves the requests to Lico (example: https://example.com). Provide the
Issuer Identifier with the `--iss` parametter when starting Lico.
Issuer Identifier with the `--iss` parameter when starting Lico.
Furthermore to allow clients to utilize the Lico services, clients need to
be known/registered. For now Lico uses a static configuration file which
@@ -149,6 +149,10 @@ The base URL of the frontend proxy is what will become the value of the `--iss`
parameter when starting up Lico. OIDC requires the Issuer Identifier to be
secure (https:// required).
## API Documentation
The complete API specification is available in OpenAPI 3.0 format at [`docs/libregraph-connect-api-v1.yaml`](docs/libregraph-connect-api-v1.yaml), covering all OIDC, OAuth2, authentication, and SAML2 endpoints with detailed schemas and examples.
### LibreGraph backend
Generic backend support is available through the LibreGraph API. Any service can

View File

@@ -144,6 +144,7 @@ func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
SignedOutEndpointURI: fullSignedOutEndpointURL,
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
DefaultBannerLogoHeight: config.IdentifierDefaultBannerLogoHeight,
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
DefaultSignInPageLogoURI: config.IdentifierDefaultLogoTargetURI,
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
@@ -159,13 +160,18 @@ func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
}
// Expose the identifier in the managers registry so other managers (e.g.
// signedlogin) can access it without creating a second instance.
bs.Managers().Set("identifier", activeIdentifier)
identityManagerConfig := &identity.Config{
SignInFormURI: fullSignInFormURL,
SignedOutURI: fullSignedOutEndpointURL,
Logger: logger,
ScopesSupported: config.Config.AllowedScopes,
ScopesSupported: config.Config.AllowedScopes,
AllowSignedLogin: config.Config.AllowClientSignedLogins,
}
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)

View File

@@ -127,6 +127,7 @@ func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
SignedOutEndpointURI: fullSignedOutEndpointURL,
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
DefaultBannerLogoHeight: config.IdentifierDefaultBannerLogoHeight,
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
DefaultSignInPageLogoURI: config.IdentifierDefaultLogoTargetURI,
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
@@ -142,13 +143,18 @@ func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
}
// Expose the identifier in the managers registry so other managers (e.g.
// signedlogin) can access it without creating a second instance.
bs.Managers().Set("identifier", activeIdentifier)
identityManagerConfig := &identity.Config{
SignInFormURI: fullSignInFormURL,
SignedOutURI: fullSignedOutEndpointURL,
Logger: logger,
ScopesSupported: config.Config.AllowedScopes,
ScopesSupported: config.Config.AllowedScopes,
AllowSignedLogin: config.Config.AllowClientSignedLogins,
}
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)

View File

@@ -206,6 +206,11 @@ func (bs *bootstrap) initialize(settings *Settings) error {
logger.Infoln("client controlled guests are enabled")
}
bs.config.Config.AllowClientSignedLogins = settings.AllowClientSignedLogins
if bs.config.Config.AllowClientSignedLogins {
logger.Infoln("client controlled signed logins are enabled")
}
bs.config.Config.AllowDynamicClientRegistration = settings.AllowDynamicClientRegistration
if bs.config.Config.AllowDynamicClientRegistration {
logger.Infoln("dynamic client registration is enabled")
@@ -257,6 +262,9 @@ func (bs *bootstrap) initialize(settings *Settings) error {
}
bs.config.IdentifierDefaultBannerLogo = b
}
if settings.IdentifierDefaultBannerLogoHeight != "" {
bs.config.IdentifierDefaultBannerLogoHeight = &settings.IdentifierDefaultBannerLogoHeight
}
if settings.IdentifierDefaultSignInPageText != "" {
bs.config.IdentifierDefaultSignInPageText = &settings.IdentifierDefaultSignInPageText
}

View File

@@ -50,6 +50,7 @@ type Config struct {
IdentifierAuthoritiesConf string
IdentifierScopesConf string
IdentifierDefaultBannerLogo []byte
IdentifierDefaultBannerLogoHeight *string
IdentifierDefaultSignInPageText *string
IdentifierDefaultLogoTargetURI *string
IdentifierDefaultUsernameHintText *string

View File

@@ -35,6 +35,7 @@ type Settings struct {
TrustedProxy []string
AllowScope []string
AllowClientGuests bool
AllowClientSignedLogins bool
AllowDynamicClientRegistration bool
EncryptionSecretFile string
Listen string
@@ -43,6 +44,7 @@ type Settings struct {
IdentifierRegistrationConf string
IdentifierScopesConf string
IdentifierDefaultBannerLogo string
IdentifierDefaultBannerLogoHeight string
IdentifierDefaultSignInPageText string
IdentifierDefaultLogoTargetURI string
IdentifierDefaultUsernameHintText string

View File

@@ -38,5 +38,6 @@ type Config struct {
AllowedScopes []string
AllowClientGuests bool
AllowClientSignedLogins bool
AllowDynamicClientRegistration bool
}

View File

@@ -21,6 +21,20 @@ clients:
# origins:
# - https://my-host:8509
# - id: playground-trusted.js
# name: Trusted OIDC Playground with External Login
# trusted: yes
# application_type: web
# redirect_uris:
# - https://my-host:8509/
# origins:
# - https://my-host:8509
# external_authorize_redirect_uris:
# # Default external login URI used for any scope.
# - https://my-external-login:8443/authorize
# # Scope-specific URI used when the given scope is requested.
# - MyApp.Special:https://my-external-login:8443/authorize-special
# - id: playground-trusted.js
# name: Trusted Insecure OIDC Playground
# trusted: yes

View File

@@ -48,6 +48,7 @@ func (i Identifier) writeHelloResponse(rw http.ResponseWriter, req *http.Request
State: r.State,
Branding: &meta.Branding{
BannerLogo: i.defaultBannerLogo,
BannerLogoHeight: i.defaultBannerLogoHeight,
UsernameHintText: i.Config.DefaultUsernameHintText,
SignInPageText: i.Config.DefaultSignInPageText,
SignInPageLogoURI: i.Config.DefaultSignInPageLogoURI,

View File

@@ -29,7 +29,7 @@ import (
"time"
"github.com/go-ldap/ldap/v3"
uuid "github.com/gofrs/uuid"
uuid "github.com/google/uuid"
"github.com/libregraph/oidc-go"
"github.com/sirupsen/logrus"
"golang.org/x/time/rate"

View File

@@ -47,6 +47,7 @@ type Config struct {
SignedOutEndpointURI *url.URL
DefaultBannerLogo []byte
DefaultBannerLogoHeight *string
DefaultSignInPageText *string
DefaultSignInPageLogoURI *string
DefaultUsernameHintText *string

View File

@@ -85,7 +85,8 @@ type Identifier struct {
meta *meta.Meta
defaultBannerLogo *string
defaultBannerLogo *string
defaultBannerLogoHeight *string
onSetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter, user identity.User) error
onUnsetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter) error
@@ -157,6 +158,7 @@ func NewIdentifier(c *Config) (*Identifier, error) {
}
i.defaultBannerLogo = &defaultBannerLogo
}
i.defaultBannerLogoHeight = c.DefaultBannerLogoHeight
i.meta.Scopes.Extend(c.Backend.ScopesMeta())
@@ -267,27 +269,26 @@ func (i *Identifier) ErrorPage(rw http.ResponseWriter, code int, title string, m
utils.WriteErrorPage(rw, code, title, message)
}
// SetUserToLogonCookie serializes the provided user into an encrypted string
// and sets it as cookie on the provided http.ResponseWriter.
func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseWriter, user *IdentifiedUser) error {
// WriteLogonCookie serializes the provided user into an encrypted string and
// sets it as a logon cookie without firing any callbacks. Use this when the
// cookie must be written mid-flow (e.g. inside an authorize handler) where
// the caller is responsible for updating browser state separately.
func (i *Identifier) WriteLogonCookie(rw http.ResponseWriter, user *IdentifiedUser) error {
loggedOn, logonAt := user.LoggedOn()
if !loggedOn {
return fmt.Errorf("refused to set cookie for not logged on user")
}
// Add standard claims.
claims := jwt.Claims{
Issuer: user.BackendName(),
Audience: audienceMarker,
Subject: user.Subject(),
IssuedAt: jwt.NewNumericDate(logonAt),
}
// Add expiration, if set.
if user.expiresAfter != nil {
claims.Expiry = jwt.NewNumericDate(*user.expiresAfter)
}
// Additional claims.
userClaims := map[string]interface{}(user.Claims())
if sessionRef := user.SessionRef(); sessionRef != nil {
userClaims[SessionIDClaim] = *sessionRef
@@ -302,25 +303,27 @@ func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseW
userClaims[LockedScopesClaim] = strings.Join(lockedScopes, " ")
}
// Serialize and encrypt cookie value.
serialized, err := jwt.Encrypted(i.encrypter).Claims(claims).Claims(userClaims).CompactSerialize()
if err != nil {
return err
}
// Set cookie.
err = i.setLogonCookie(rw, serialized)
if err != nil {
return i.setLogonCookie(rw, serialized)
}
// SetUserToLogonCookie serializes the provided user into an encrypted string,
// sets it as cookie on the provided http.ResponseWriter, and fires the
// onSetLogon callbacks.
func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseWriter, user *IdentifiedUser) error {
if err := i.WriteLogonCookie(rw, user); err != nil {
return err
}
// Trigger callbacks.
for _, f := range i.onSetLogonCallbacks {
err = f(ctx, rw, user)
if err != nil {
if err := f(ctx, rw, user); err != nil {
return err
}
}
return nil
}

View File

@@ -20,6 +20,7 @@ package meta
// Branding is a container to hold identifier branding meta data.
type Branding struct {
BannerLogo *string `json:"bannerLogo,omitempty"`
BannerLogoHeight *string `json:"bannerLogoHeight,omitempty"`
SignInPageText *string `json:"signinPageText,omitempty"`
UsernameHintText *string `json:"usernameHintText,omitempty"`
SignInPageLogoURI *string `json:"signinPageLogoURI,omitempty"`

View File

@@ -158,7 +158,7 @@ type Consent struct {
}
// Scopes returns the associated consents approved scopes filtered by the
//provided requested scopes and the full unfiltered approved scopes table.
// provided requested scopes and the full unfiltered approved scopes table.
func (c *Consent) Scopes(requestedScopes map[string]bool) (map[string]bool, map[string]bool) {
scopes := make(map[string]bool)
if c.RawScope != "" {

View File

@@ -146,6 +146,12 @@ func (u *IdentifiedUser) LoggedOn() (bool, time.Time) {
return !u.logonAt.IsZero(), u.logonAt
}
// SetLogonAt sets the logon timestamp, marking the user as logged on. Used by
// flows that authenticate without a password challenge (e.g. signed login).
func (u *IdentifiedUser) SetLogonAt(t time.Time) {
u.logonAt = t
}
// SessionRef returns the accociated users underlaying session reference.
func (u *IdentifiedUser) SessionRef() *string {
return u.sessionRef

View File

@@ -23,6 +23,8 @@ import (
"crypto/subtle"
"encoding/base64"
"fmt"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
@@ -52,7 +54,8 @@ type ClientRegistration struct {
TrustedScopes []string `yaml:"trusted_scopes" json:"-"`
Insecure bool `yaml:"insecure" json:"-"`
ImplicitScopes []string `yaml:"implicit_scopes" json:"-"`
ImplicitScopes []string `yaml:"implicit_scopes" json:"-"`
ExternalAuthorizeRedirectURIs []string `yaml:"external_authorize_redirect_uris,flow" json:"-"`
Dynamic bool `yaml:"-" json:"-"`
IDIssuedAt time.Time `yaml:"-" json:"-"`
@@ -81,6 +84,23 @@ type ClientRegistration struct {
// Validate validates the associated client registration data and returns error
// if the data is not valid.
func (cr *ClientRegistration) Validate() error {
for _, entry := range cr.ExternalAuthorizeRedirectURIs {
uri := entry
// Strip scope prefix if present using the colon heuristic.
if idx := strings.Index(entry, ":"); idx > 0 {
rest := entry[idx+1:]
if !strings.HasPrefix(rest, "//") {
uri = rest
}
}
parsed, err := url.Parse(uri)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return fmt.Errorf("invalid external_authorize_redirect_uri: %v", entry)
}
if parsed.Scheme != "https" {
return fmt.Errorf("external_authorize_redirect_uri must use https: %v", entry)
}
}
return nil
}
@@ -200,6 +220,46 @@ func (cr *ClientRegistration) ApplyImplicitScopes(scopes map[string]bool) error
return nil
}
// GetExternalAuthorizeRedirectURI returns the external authorize redirect URI
// for the given scopes. A scope-specific match takes precedence over the
// default. Returns empty string if none is configured.
func (cr *ClientRegistration) GetExternalAuthorizeRedirectURI(scopes map[string]bool) string {
if len(cr.ExternalAuthorizeRedirectURIs) == 0 {
return ""
}
var defaultURI string
scopedURIs := make(map[string]string)
for _, entry := range cr.ExternalAuthorizeRedirectURIs {
if idx := strings.Index(entry, ":"); idx > 0 {
rest := entry[idx+1:]
// If the remainder starts with "//", this is a plain URL with
// no scope prefix (e.g. https://...).
if !strings.HasPrefix(rest, "//") {
scopedURIs[entry[:idx]] = rest
continue
}
}
if defaultURI == "" {
defaultURI = entry
}
}
// Try to find a scope-specific match.
if scopes != nil {
for scope, ok := range scopes {
if !ok {
continue
}
if u, found := scopedURIs[scope]; found {
return u
}
}
}
return defaultURI
}
func (cr *ClientRegistration) makeSecret(secret []byte) (string, string, error) {
// Create random secret. HMAC the client name with it to get the subject.
if secret == nil {

View File

@@ -89,13 +89,14 @@ func NewRegistry(ctx context.Context, trustedURI *url.URL, registrationConfFilep
validateErr := client.Validate()
registerErr := r.Register(client)
fields := logrus.Fields{
"client_id": client.ID,
"with_client_secret": client.Secret != "",
"trusted": client.Trusted,
"insecure": client.Insecure,
"application_type": client.ApplicationType,
"redirect_uris": client.RedirectURIs,
"origins": client.Origins,
"client_id": client.ID,
"with_client_secret": client.Secret != "",
"trusted": client.Trusted,
"insecure": client.Insecure,
"application_type": client.ApplicationType,
"redirect_uris": client.RedirectURIs,
"origins": client.Origins,
"external_authorize_redirect_uris": client.ExternalAuthorizeRedirectURIs,
}
if validateErr != nil {

View File

@@ -30,5 +30,8 @@ type Config struct {
ScopesSupported []string
// AllowSignedLogin enables the signed JWT auto sign-in flow.
AllowSignedLogin bool
Logger logrus.FieldLogger
}

View File

@@ -23,12 +23,16 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/clients"
@@ -47,9 +51,17 @@ type IdentifierIdentityManager struct {
scopesSupported []string
claimsSupported []string
identifier *identifier.Identifier
clients *clients.Registry
logger logrus.FieldLogger
identifier *identifier.Identifier
clients *clients.Registry
guestManager identity.Manager
logger logrus.FieldLogger
allowSignedLogin bool
// JTI replay prevention store for the signed login flow.
jtiMu sync.Mutex
jtiStore map[string]time.Time
jtiMaxAge time.Duration
}
type identifierUser struct {
@@ -108,8 +120,11 @@ func NewIdentifierIdentityManager(c *identity.Config, i *identifier.Identifier)
oidc.EmailVerifiedClaim,
},
identifier: i,
logger: c.Logger,
identifier: i,
logger: c.Logger,
allowSignedLogin: c.AllowSignedLogin,
jtiStore: make(map[string]time.Time),
jtiMaxAge: 10 * time.Minute,
}
return im
@@ -119,9 +134,26 @@ func NewIdentifierIdentityManager(c *identity.Config, i *identifier.Identifier)
func (im *IdentifierIdentityManager) RegisterManagers(mgrs *managers.Managers) error {
im.clients = mgrs.Must("clients").(*clients.Registry)
// Wire guest manager as fallback if available.
if guestManager, ok := mgrs.Get("guest"); ok && guestManager != nil {
im.guestManager = guestManager.(identity.Manager)
}
return im.identifier.RegisterManagers(mgrs)
}
// getSignInFormURI returns the sign-in form URI for the given client and
// scopes. If the client has a configured external authorize redirect URI, it
// is returned. Otherwise the default sign-in form URI is used.
func (im *IdentifierIdentityManager) getSignInFormURI(clientID string, scopes map[string]bool) string {
if registration, ok := im.clients.Get(context.Background(), clientID); ok && registration != nil {
if uri := registration.GetExternalAuthorizeRedirectURI(scopes); uri != "" {
return uri
}
}
return im.signInFormURI
}
// Authenticate implements the identity.Manager interface.
func (im *IdentifierIdentityManager) Authenticate(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, next identity.Manager) (identity.AuthRecord, error) {
var user *identifierUser
@@ -132,7 +164,14 @@ func (im *IdentifierIdentityManager) Authenticate(ctx context.Context, rw http.R
return nil, ar.NewError(authenticationErrorID, req.Form.Get("error_description"))
}
// When signed login is enabled and a signed JWT request is present, handle
// the signed login flow directly and bypass any existing session cookie.
if im.allowSignedLogin && ar.Scopes[konnect.ScopeSignedLoginOK] && ar.Request != nil {
return im.authenticateSignedLogin(ctx, rw, req, ar)
}
u, _ := im.identifier.GetUserFromLogonCookie(ctx, req, ar.MaxAge, true)
if u != nil {
// TODO(longsleep): Add other user meta data.
user = asIdentifierUser(u)
@@ -254,7 +293,7 @@ func (im *IdentifierIdentityManager) Authenticate(ctx context.Context, rw http.R
query.Set("claims_scope", strings.Join(claimsScopes, " "))
}
}
u, _ := url.Parse(im.signInFormURI)
u, _ := url.Parse(im.getSignInFormURI(ar.ClientID, ar.Scopes))
u.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, u, nil, false)
@@ -278,6 +317,11 @@ func (im *IdentifierIdentityManager) Authenticate(ctx context.Context, rw http.R
// Authorize implements the identity.Manager interface.
func (im *IdentifierIdentityManager) Authorize(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth identity.AuthRecord) (identity.AuthRecord, error) {
// Route signed login authorizations through their own path.
if im.allowSignedLogin && ar.Scopes[konnect.ScopeSignedLoginOK] && ar.Request != nil {
return im.authorizeSignedLogin(ctx, rw, req, ar, auth)
}
promptConsent := false
var approvedScopes map[string]bool
@@ -407,6 +451,149 @@ func (im *IdentifierIdentityManager) Authorize(ctx context.Context, rw http.Resp
return auth, nil
}
// checkAndRecordJTI checks the JTI claim for replay and records it if new.
func (im *IdentifierIdentityManager) checkAndRecordJTI(ar *payload.AuthenticationRequest) error {
roc, ok := ar.Request.Claims.(*payload.RequestObjectClaims)
if !ok {
return nil
}
jti := roc.ID
if jti == "" {
return fmt.Errorf("IdentifierIdentityManager: missing or invalid jti claim")
}
now := time.Now()
exp := now.Add(im.jtiMaxAge)
// Use the JWT expiry as TTL if shorter than the max age.
if expTime, err := roc.GetExpirationTime(); err == nil && expTime != nil {
if expTime.Time.Before(exp) {
exp = expTime.Time
}
}
im.jtiMu.Lock()
defer im.jtiMu.Unlock()
// Purge expired entries.
for k, v := range im.jtiStore {
if now.After(v) {
delete(im.jtiStore, k)
}
}
if _, exists := im.jtiStore[jti]; exists {
return fmt.Errorf("IdentifierIdentityManager: replayed jti")
}
im.jtiStore[jti] = exp
return nil
}
// authenticateSignedLogin handles the signed JWT auto sign-in flow.
func (im *IdentifierIdentityManager) authenticateSignedLogin(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest) (identity.AuthRecord, error) {
if ar.Request.Method == jwt.SigningMethodNone {
return nil, ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "IdentifierIdentityManager: request object must be signed")
}
roc, ok := ar.Request.Claims.(*payload.RequestObjectClaims)
if !ok || roc.Claims == nil || ar.Claims == nil || ar.Claims.IDToken == nil {
return nil, ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "IdentifierIdentityManager: missing claims in request object")
}
// Extract the login hint from the signed claims.
loginHint, ok := ar.Claims.IDToken.GetStringValue(oidc.PreferredUsernameClaim)
if !ok || loginHint == "" {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "IdentifierIdentityManager: missing preferred_username claim")
}
// JTI replay prevention.
if err := im.checkAndRecordJTI(ar); err != nil {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
// Look up user from the scoped backend.
u, err := im.identifier.GetUserFromID(ctx, loginHint, nil, ar.Scopes)
if err != nil {
im.logger.WithError(err).Errorln("IdentifierIdentityManager: signed login backend error")
return nil, ar.NewError(oidc.ErrorCodeOAuth2ServerError, "IdentifierIdentityManager: backend error")
}
if u == nil {
return nil, ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, "IdentifierIdentityManager: user not found")
}
user := asIdentifierUser(u)
if err := ar.Verify(user.Subject()); err != nil {
return nil, err
}
// Set logon time and write a session cookie so that subsequent
// authorization requests (silent renew, re-auth) succeed via the normal
// cookie path without requiring a new signed JWT each time.
authTime := time.Now()
u.SetLogonAt(authTime)
if cookieErr := im.identifier.WriteLogonCookie(rw, u); cookieErr != nil {
im.logger.WithError(cookieErr).Warnln("IdentifierIdentityManager: failed to set logon cookie")
}
auth := identity.NewAuthRecord(im, user.Subject(), nil, nil, nil)
auth.SetUser(user)
auth.SetAuthTime(authTime)
return auth, nil
}
// authorizeSignedLogin handles scope approval for the signed JWT auto sign-in flow.
func (im *IdentifierIdentityManager) authorizeSignedLogin(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth identity.AuthRecord) (identity.AuthRecord, error) {
if ar.Request == nil {
return nil, ar.NewError(oidc.ErrorCodeOIDCInvalidRequestObject, "IdentifierIdentityManager: authorize without request object")
}
roc, ok := ar.Request.Claims.(*payload.RequestObjectClaims)
if !ok {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "IdentifierIdentityManager: authorize with invalid claims request")
}
securedDetails := roc.Secure()
if securedDetails == nil {
return nil, ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "IdentifierIdentityManager: authorize without secure client")
}
clientDetails, err := im.clients.Lookup(req.Context(), ar.ClientID, "", ar.RedirectURI, "", true)
if err != nil {
return nil, ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, err.Error())
}
if clientDetails.ID != securedDetails.ID {
return nil, ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, "client mismatch")
}
var approvedScopes map[string]bool
if clientDetails.Trusted && securedDetails.TrustedScopes == nil {
approvedScopes = ar.Scopes
} else {
// Approve openid plus any scope in the client's trusted_scopes list.
approvedScopes = make(map[string]bool)
for _, scope := range securedDetails.TrustedScopes {
if ar.Scopes[scope] {
approvedScopes[scope] = true
}
}
if ar.Scopes[oidc.ScopeOpenID] {
approvedScopes[oidc.ScopeOpenID] = true
}
// Ensure the signed-login scope was approved.
if !approvedScopes[konnect.ScopeSignedLoginOK] {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "IdentifierIdentityManager: client does not authorize "+konnect.ScopeSignedLoginOK+" scope")
}
}
auth.AuthorizeScopes(approvedScopes)
auth.AuthorizeClaims(ar.Claims)
return auth, nil
}
// EndSession implements the identity.Manager interface.
func (im *IdentifierIdentityManager) EndSession(ctx context.Context, rw http.ResponseWriter, req *http.Request, esr *payload.EndSessionRequest) error {
var err error

View File

@@ -90,6 +90,7 @@ func (p *Provider) JwksHandler(rw http.ResponseWriter, req *http.Request) {
func (p *Provider) AuthorizeHandler(rw http.ResponseWriter, req *http.Request) {
var err error
var auth identity.AuthRecord
var nextManager identity.Manager
addResponseHeaders(rw.Header())
@@ -173,7 +174,8 @@ func (p *Provider) AuthorizeHandler(rw http.ResponseWriter, req *http.Request) {
// Authorization Server Authenticates End-User
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthenticates
auth, err = p.identityManager.Authenticate(ctx, rw, req, ar, p.guestManager)
nextManager = p.guestManager
auth, err = p.identityManager.Authenticate(ctx, rw, req, ar, nextManager)
if err != nil {
goto done
}

View File

@@ -28,4 +28,7 @@ const (
// ScopeGuestOK is the string value for the built-in Guest OK scope.
ScopeGuestOK = "LibreGraph.GuestOK"
// ScopeSignedLoginOK is the string value for the built-in Signed Login OK scope.
ScopeSignedLoginOK = "LibreGraph.SignedLoginOK"
)

7
vendor/modules.txt vendored
View File

@@ -682,9 +682,6 @@ github.com/goccy/go-yaml/token
# github.com/gofrs/flock v0.13.0
## explicit; go 1.24.0
github.com/gofrs/flock
# github.com/gofrs/uuid v4.4.0+incompatible
## explicit
github.com/gofrs/uuid
# github.com/gogo/protobuf v1.3.2
## explicit; go 1.15
github.com/gogo/protobuf/gogoproto
@@ -969,8 +966,8 @@ github.com/libregraph/idm/server
github.com/libregraph/idm/server/handler
github.com/libregraph/idm/server/handler/boltdb
github.com/libregraph/idm/server/handler/ldif
# github.com/libregraph/lico v0.66.0
## explicit; go 1.23.0
# github.com/libregraph/lico v0.67.0
## explicit; go 1.24.0
github.com/libregraph/lico
github.com/libregraph/lico/bootstrap
github.com/libregraph/lico/bootstrap/backends/guest