Documentation
¶
Overview ¶
federated.go — федеративная полоса приёма: утверждение подписал ВНЕШНИЙ издатель, а перечень доверенных издателей ведём мы (задача #1124, RFC 7523 §2.1 — вид выдачи, не аутентификация клиента).
Чем эта полоса отличается от полосы аутентификации клиента ¶
Там издатель и субъект обязаны совпадать и оба назвать НАШУ строку, а ключ берётся из строки клиента. Здесь издатель — посторонний, субъект — его собственный, ключ принадлежит ему, и единственное, что есть у нас, — ЗАПИСЬ О ДОВЕРИИ. Промах поэтому даёт не «приняли своё за чужое», а токен платформы предъявителю, которого мы не заводили вовсе.
Три признака, отделяющих утверждение от нашего же токена доступа ¶
На полосе клиента первый признак — объявленный нами тип в заголовке. Здесь его нет и быть не может: тип ставит производитель, а производитель тут посторонний, и требование типа отвергало бы каждого обычного издателя. Признаки, которые остаются и которых достаточно:
- **ключ.** Подпись проверяется ключом ИЗДАТЕЛЯ из записи доверия. Наш токен доступа подписан нашим ключом, которого в этой таблице нет;
- **наш идентификатор издателя в этой таблице ЗАПРЕЩЁН** — иначе первый признак снимался бы одной административной записью, и наш собственный токен доступа стал бы приниматься как чужое утверждение;
- **адресат.** Утверждение адресовано нашему издателю; токен доступа адресован ресурсу.
Package clientassertion — принимающая сторона аутентификации клиента подписанным утверждением (RFC 7523 §2.2, задача #898).
Чем эта сторона отличается от предъявляющей ¶
Утверждение этого вида дерево уже подписывает и предъявляет; вся та работа стоит на стороне КЛИЕНТА. Здесь заводится сторона СЕРВЕРА, и цена ошибки у неё противоположна: у предъявителя неверное утверждение даёт отказ, видимый сразу, — у принимающего неверная проверка даёт ПРИНЯТОЕ ЧУЖОЕ утверждение, не видимое никогда. Успешная аутентификация выглядит одинаково независимо от того, что именно она проверила.
Отсюда правило работ, которому подчинён весь порядок ниже: на каждой развилке, где неясно, отвергать или пропускать, выбирается ОТВЕРГАТЬ.
Почему разбор идёт над той же библиотекой, а не своей ¶
Стеков разбора JOSE в дереве и без нас несколько, и решения, принятые здесь (закрытый словарь алгоритмов, отказ на подмену алгоритма, дублирующееся имя члена заголовка, член, помеченный обязательным к пониманию), доехали бы только до одного из них. Второй библиотеки «ради удобства утверждения» не заводится: проверки, которых библиотека не делает, добавляются шагом НАД ней.
Что здесь делается ДО библиотеки и почему именно это ¶
Три вещи библиотека не делает by construction, и каждая из них — отказ:
- **имя члена заголовка дважды.** Разбор объектной записи, встретив имя дважды, берёт одно из значений и не возражает. Два значения одного члена означают, что проверяющий и подписант МОГЛИ ПРОЧИТАТЬ РАЗНОЕ, а значит проверенной оказалась не та половина (RFC 7515 §5.2);
- **член, помеченный обязательным к пониманию.** Пометка означает «без понимания этого члена подпись нельзя считать проверенной». Принять — значит объявить проверенным то, чего не поняли;
- **встроенный ключевой материал.** Ключ выбирается ТОЛЬКО из реестра; перечень членов, из которых он выбираться не может, объявлен в pkg/tokenpolicy одним местом.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func PresenterResponseFor ¶
PresenterResponseFor — опознавательное слово стандартной формы.
Одно и то же для ВСЕХ исходов отказа аутентификации. Функция, а не константа: вызывающему нужен именно ответ, а не строка, и подмена одного другим на каком-то из путей была бы ровно тем расхождением, которого мы избегаем.
Types ¶
type ClientResolver ¶
type ClientResolver interface {
ResolveAssertionClient(ctx context.Context, clientID string) (domain.AssertionClient, error)
}
ClientResolver — порт реестра, СПОСОБНОГО к утверждению.
«Способного» здесь несущее слово: разрешение идёт только по таблицам, несущим ключевой материал. Вид клиента, у которого нет способа доказать владение ключом by construction, не резолвится ни во что, и отказ наступает на том же шаге, что для несуществующего.
type Outcome ¶
type Outcome string
Outcome — исход проверки. Закрытый словарь: у КАЖДОГО значения свой счётчик, иначе мёртвый контроль невидим — проверка, не отказавшая ни разу за всё время жизни, неотличима от проверки, которая работает и просто не встречала нарушителя.
const ( OutcomeAccepted Outcome = "accepted" OutcomeAssertionTypeMismatch Outcome = "assertion-type-mismatch" OutcomeMalformedSerialization Outcome = "malformed-serialization" OutcomeDuplicateHeaderMember Outcome = "duplicate-header-member" OutcomeUnsupportedCritical Outcome = "unsupported-critical-member" // #nosec G101 -- машинный ПРИЗНАК ПРИЧИНЫ ОТКАЗА, уезжающий в журнал и в // счётчик, а не секрет: значения этого словаря наружу не выходят вовсе, // потому что ответ предъявителю у всех исходов один. OutcomeTokenTypeMismatch Outcome = "token-type-mismatch" OutcomeAlgorithmNotAllowed Outcome = "algorithm-not-allowed" OutcomeAlgorithmMismatch Outcome = "algorithm-mismatch" OutcomeIdentityMismatch Outcome = "identity-mismatch" OutcomeClientUnknown Outcome = "client-unknown" OutcomeClientCannotAssert Outcome = "client-cannot-assert" // OutcomeIssuerUntrusted — пара (издатель, субъект) не резолвится в нашем // перечне доверенных издателей. Один исход на все состояния «мы за это не // ручаемся»: различимые дали бы предъявителю оракул состава перечня. OutcomeIssuerUntrusted Outcome = "issuer-untrusted" // OutcomeTrustExpired — запись доверия найдена, её срок истёк. Отдельный // счётчик от предыдущего: «доверия не было» и «доверие кончилось» суть // разные события эксплуатации, и слитые в один счётчик они делают // истечение невидимым. OutcomeTrustExpired Outcome = "issuer-trust-expired" OutcomeSignatureMismatch Outcome = "signature-mismatch" OutcomeAudienceMismatch Outcome = "audience-mismatch" OutcomeExpiryMissing Outcome = "expiry-missing" OutcomeIssuedAtMissing Outcome = "issued-at-missing" OutcomeIssuedAtInFuture Outcome = "issued-at-in-future" OutcomeLifetimeAboveCeiling Outcome = "lifetime-above-ceiling" OutcomeExpired Outcome = "expired" OutcomeNotYetValid Outcome = "not-yet-valid" OutcomeAssertionIDMissing Outcome = "assertion-id-missing" OutcomeReplayed Outcome = "replayed" // OutcomeMethodNotAllowed — обращение методом, отличным от объявленного. OutcomeMethodNotAllowed Outcome = "method-not-allowed" // OutcomeBodyAboveCeiling — объявленная длина либо само тело сверх потолка. OutcomeBodyAboveCeiling Outcome = "body-above-ceiling" // OutcomeMalformedRequest — тело не разбирается как форма запроса. OutcomeMalformedRequest Outcome = "malformed-request" // OutcomeMultipleAssertions — параметр с утверждением встретился дважды. // «Ровно одно утверждение» есть требование к НАШЕМУ разбору, а не описание // намерения клиента. OutcomeMultipleAssertions Outcome = "multiple-assertions" // OutcomeUnsupportedGrantType — вид выдачи вне закрытого перечня. OutcomeUnsupportedGrantType Outcome = "unsupported-grant-type" // OutcomeAudienceNotAllowed — запрошенный адресат вне объявленного // конфигурацией перечня адресатов платформы. OutcomeAudienceNotAllowed Outcome = "requested-audience-not-allowed" // OutcomeClientExpired — срок клиента истёк. OutcomeClientExpired Outcome = "client-expired" // OutcomeOwnerNotActive — владелец клиента не в состоянии ACTIVE. OutcomeOwnerNotActive Outcome = "owner-not-active" // OutcomeIssuanceFailed — аутентификация прошла, выпуск не состоялся. OutcomeIssuanceFailed Outcome = "issuance-failed" )
func Outcomes ¶
func Outcomes() []Outcome
Outcomes возвращает закрытый словарь исходов целиком.
ВЫВОДИТСЯ отсюда всяким, кому нужен перечень: набор счётчиков, проба переписи, текст отказа. Вторая копия словаря разошлась бы молча, и разошлась бы она в сторону «исход без счётчика» — то есть в сторону невидимости.
type Policy ¶
type Policy struct {
// ExpectedAudience — идентификатор НАШЕГО издателя. Единственная
// принимаемая форма адресата утверждения.
ExpectedAudience string
// MaxLifetime — потолок разницы «срок − момент выпуска» на полосе клиента.
MaxLifetime time.Duration
// MaxFederatedLifetime — тот же потолок на федеративной полосе.
//
// Отдельное поле, а не то же число: утверждение полосы клиента выписывает
// НАШ клиент специально для нас и вправе дать ему минуты, а внешний
// издатель выпускает своей нагрузке токен со своим сроком и о нашем
// потолке не знает. Одно число на две полосы означало бы либо отказ
// каждому внешнему издателю, либо месячное окно у собственного клиента.
MaxFederatedLifetime time.Duration
// ClockSkew — допуск расхождения часов. Действует на ОБЕ стороны: и на
// истечение, и на момент выпуска в будущем.
ClockSkew time.Duration
// Clock — источник времени. Вход, а не окружение: граничные сценарии
// (ровно в момент истечения, ровно в момент начала действия) на системных
// часах поставить нельзя вовсе, и их отсутствие неотличимо от полноты.
Clock func() time.Time
}
Policy — объявленная настройка проверяющего.
Каждое поле ОБЯЗАТЕЛЬНО, и это не педантизм: незаданный ожидаемый адресат означает «принимаем любой», незаданный потолок длительности — «любую», неподанные часы — «читаем окружение». Пустое значение здесь означает «не сужаем», а не «по умолчанию».
type ReplayGuard ¶
type ReplayGuard interface {
Redeem(ctx context.Context, clientID, assertionID string, expiresAt time.Time) error
}
ReplayGuard — порт однократности.
type Result ¶
type Result struct {
// Outcome — что именно решило исход. Для ЖУРНАЛА и СЧЁТЧИКА, никогда для
// ответа предъявителю.
Outcome Outcome
// Client — разрешённая строка реестра. Заполняется только при приёме.
Client domain.AssertionClient
// AssertionID — погашенный идентификатор однократности.
AssertionID string
// ExpiresAt — момент истечения предъявленного утверждения.
ExpiresAt time.Time
}
Result — исход проверки вместе с тем, что вправе узнать вызывающий.
func Refuse ¶
Refuse собирает отказ названного исхода за пределами проверяющего — на эндпоинте и на выдаче.
Экспортирована, чтобы отказ СОБИРАЛСЯ ОДНИМ СПОСОБОМ везде: вторая сборка разошлась бы с этой в том, что видит предъявитель, и разошлась бы молча.
func (Result) PresenterResponse ¶
PresenterResponse — опознавательное слово, которое уходит предъявителю.
Метод, а не поле: поле пришлось бы заполнять на каждом пути отказа, и путь, забывший его заполнить, отдавал бы пустую строку — то есть отличался бы от остальных ровно тем, чего мы избегаем.
type TrustedIssuerResolver ¶
type TrustedIssuerResolver interface {
ResolveTrustedIssuer(ctx context.Context, issuer, subject string) (
domain.TrustedIssuer, domain.AssertionClient, error)
}
TrustedIssuerResolver — порт НАШЕГО перечня доверенных издателей.
Возвращает и запись доверия, и строку, которую она уполномочивает, ОДНИМ обращением. Два обращения дали бы разную длительность ответа у пары, дошедшей до второго чтения, и у пары, отвергнутой на первом, — то есть оракул, сообщающий, доверен ли издатель, ничего не отвечая по существу.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier — проверяющий утверждение клиента.
func New ¶
func New(p Policy, clients ClientResolver, issuers TrustedIssuerResolver, replay ReplayGuard) (*Verifier, error)
New строит проверяющего. Неполная настройка — ОТКАЗ ПОСТРОЕНИЯ.
Проверяющий, собранный наполовину, принимал бы утверждения, которые обязан отвергнуть, — и узналось бы это не на старте, а на первом принятом чужом предъявлении, то есть никогда.
func (*Verifier) DeclaredChecks ¶
func (v *Verifier) DeclaredChecks() []tokenpolicy.Check
DeclaredChecks возвращает состав проверок ЭТОГО проверяющего.
Объявление существует затем, чтобы его можно было СВЕРИТЬ с единым перечнем (`tokenpolicy.MandatoryChecks`), а не читать четыре реализации глазами. Этот проверяющий — четвёртый ответ на тот же вопрос «годен ли предъявленный подписанный материал», и он появился ровно так, как предсказывала задача, заводившая единый перечень: вместе с новой поверхностью приёма.
Запись, которой проверяющий не исполняет, отсюда нельзя: тогда объявление станет вторым местом об одном предмете и разойдётся молча.
func (*Verifier) DeclaredDeviations ¶
func (v *Verifier) DeclaredDeviations() []tokenpolicy.Deviation
DeclaredDeviations — обязательные проверки, которых этот проверяющий НЕ исполняет, вместе с причиной.
Причина про РАЗНИЦУ ПРЕДМЕТА, а не про очередь работ: утверждение клиента — не токен доступа. Оно предъявляется РОВНО ОДИН раз, на обмен, и его однократность держит страж повтора (`ReplayGuard`) — механизм строже отзыва: отзыв запрещает предъявлять снова начиная с момента, страж — вообще. Спрашивать авторитет отзыва о материале, который живёт минуты и принимается единожды, нечего.
func (*Verifier) Verify ¶
Verify проверяет предъявленное утверждение.
Порядок проверок — часть решения, а не деталь ¶
Он выбран так, чтобы дорогое стояло после дешёвого, а решения о ключе — после того, как ключ разрешён по РЕЕСТРУ:
вид предъявления → форма → заголовок (дубли · пометки · ТИП) → алгоритм словаря → личность → реестр → алгоритм КЛИЕНТА → подпись → адресат → время → однократность
Две границы в нём несущие. Сверка алгоритма с ЗАРЕГИСТРИРОВАННЫМ у клиента стоит ДО проверки подписи: перечень допустимых алгоритмов строится из строки реестра, никогда из заголовка предъявленного утверждения. И погашение однократности стоит ПОСЛЕДНИМ: гасить идентификатор утверждения, которое всё равно будет отвергнуто, значило бы дать предъявителю способ занимать чужие ключи ненадёжными утверждениями.
func (*Verifier) VerifyFederated ¶
VerifyFederated проверяет утверждение, подписанное внешним издателем, и разрешает его в НАШУ строку через перечень доверенных издателей.
Порядок — тот же довод, что и на полосе клиента ¶
Дорогое после дешёвого, решения о ключе — после того, как ключ разрешён по ЗАПИСИ ДОВЕРИЯ, погашение однократности — последним:
форма → заголовок (дубли · пометки) → алгоритм словаря → личность → наш издатель запрещён → перечень доверия → срок доверия → алгоритм ЗАПИСИ → подпись → адресат → время → однократность