mirror of https://github.com/grafana/grafana
Auth: Azure AD OAuth (#20030)
* Implement Azure AD oauth * Use go-jose and cleanup * Update go-jose in go.mod * cleanup * Add unit tests * Fix scopes * Add documentation page * Improve documentation * Convert extract_role into function. * Do not use upn and replace unique_name with preferred_username * Configure login button * Use official microsoft icon and color from branding guideline. * Add Azure AD config section in sample.ini.pull/22173/head
parent
ceca067d4f
commit
ff6a082e23
@ -0,0 +1,109 @@ |
|||||||
|
+++ |
||||||
|
title = "Azure AD OAuth2 Authentication" |
||||||
|
description = "Grafana OAuthentication Guide " |
||||||
|
keywords = ["grafana", "configuration", "documentation", "oauth"] |
||||||
|
type = "docs" |
||||||
|
[menu.docs] |
||||||
|
name = "Azure AD" |
||||||
|
identifier = "azuread_oauth2" |
||||||
|
parent = "authentication" |
||||||
|
weight = 3 |
||||||
|
+++ |
||||||
|
|
||||||
|
# Azure AD OAuth2 Authentication |
||||||
|
|
||||||
|
The Azure AD authentication provides the possibility to use an Azure Active Directory tenant as an identity provider for Grafana. |
||||||
|
|
||||||
|
By using Azure AD Application Roles it is also possible to assign Users and Groups to Grafana roles from the Azure Portal. |
||||||
|
|
||||||
|
To enable the Azure AD OAuth2 you must register your application with Azure AD. |
||||||
|
|
||||||
|
# Create Azure AD application |
||||||
|
|
||||||
|
1. Log in to [Azure Portal](https://portal.azure.com) and click **Azure Active Directory** in the side menu. |
||||||
|
|
||||||
|
1. Click **App Registrations** and add a new application registration: |
||||||
|
- Name: Grafana |
||||||
|
- Application type: Web app / API |
||||||
|
- Sign-on URL: `https://<grafana domain>/login/azuread` |
||||||
|
|
||||||
|
1. Click the name of the new application to open the application details page. |
||||||
|
|
||||||
|
1. Click **Endpoints**. |
||||||
|
- Note down the **OAuth 2.0 authorization endpoint (v2)**, this will be the auth url. |
||||||
|
- Note down the **OAuth 2.0 token endpoint (v2)**, this will be the token url. |
||||||
|
|
||||||
|
1. Close the Endpoints page to come back to the application details page. |
||||||
|
|
||||||
|
1. Note down the "Application ID", this will be the OAuth client id. |
||||||
|
|
||||||
|
1. Click **Certificates & secrets** and add a new entry under Client secrets. |
||||||
|
- Description: Grafana OAuth |
||||||
|
- Expires: Never |
||||||
|
|
||||||
|
1. Click **Add** then copy the key value, this will be the OAuth client secret. |
||||||
|
|
||||||
|
1. Click **Manifest**. |
||||||
|
- Add definitions for the required Application Roles for Grafana (Viewer, Editor, Admin). Without this configuration all users will be assigned to the Viewer role. |
||||||
|
- Every role has to have a unique id. On Linux this can be created with `uuidgen` for instance. |
||||||
|
|
||||||
|
```json |
||||||
|
"appRoles": [ |
||||||
|
{ |
||||||
|
"allowedMemberTypes": [ |
||||||
|
"User" |
||||||
|
], |
||||||
|
"description": "Grafana admin Users", |
||||||
|
"displayName": "Grafana Admin", |
||||||
|
"id": "SOME_UNIQUE_ID", |
||||||
|
"isEnabled": true, |
||||||
|
"lang": null, |
||||||
|
"origin": "Application", |
||||||
|
"value": "Admin" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"allowedMemberTypes": [ |
||||||
|
"User" |
||||||
|
], |
||||||
|
"description": "Grafana read only Users", |
||||||
|
"displayName": "Grafana Viewer", |
||||||
|
"id": "SOME_UNIQUE_ID", |
||||||
|
"isEnabled": true, |
||||||
|
"lang": null, |
||||||
|
"origin": "Application", |
||||||
|
"value": "Viewer" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"allowedMemberTypes": [ |
||||||
|
"User" |
||||||
|
], |
||||||
|
"description": "Grafana Editor Users", |
||||||
|
"displayName": "Grafana Editor", |
||||||
|
"id": "SOME_UNIQUE_ID", |
||||||
|
"isEnabled": true, |
||||||
|
"lang": null, |
||||||
|
"origin": "Application", |
||||||
|
"value": "Editor" |
||||||
|
} |
||||||
|
], |
||||||
|
``` |
||||||
|
|
||||||
|
1. Click Overview and then on **Managed application in local directory** to show the Enterprise Application details. |
||||||
|
|
||||||
|
1. Click on **Users and groups** and add Users/Groups to the Grafana roles by using **Add User**. |
||||||
|
|
||||||
|
1. Add the following to the [Grafana configuration file]({{< relref "../installation/configuration.md#config-file-locations" >}}): |
||||||
|
|
||||||
|
```ini |
||||||
|
[auth.azuread] |
||||||
|
name = Azure AD |
||||||
|
enabled = true |
||||||
|
allow_sign_up = true |
||||||
|
client_id = APPLICATION_ID |
||||||
|
client_secret = CLIENT_SECRET |
||||||
|
scopes = openid email profile |
||||||
|
auth_url = https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/authorize |
||||||
|
token_url = https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token |
||||||
|
``` |
||||||
|
|
||||||
|
> Note: Ensure that the [root_url]({{< relref "../installation/configuration/#root-url" >}}) in Grafana is set in your Azure Application Reply URLs (App -> Settings -> Reply URLs) |
||||||
@ -0,0 +1,111 @@ |
|||||||
|
package social |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models" |
||||||
|
|
||||||
|
"golang.org/x/oauth2" |
||||||
|
"gopkg.in/square/go-jose.v2/jwt" |
||||||
|
) |
||||||
|
|
||||||
|
type SocialAzureAD struct { |
||||||
|
*SocialBase |
||||||
|
allowedDomains []string |
||||||
|
allowSignup bool |
||||||
|
} |
||||||
|
|
||||||
|
type azureClaims struct { |
||||||
|
Email string `json:"email"` |
||||||
|
PreferredUsername string `json:"preferred_username"` |
||||||
|
Roles []string `json:"roles"` |
||||||
|
Name string `json:"name"` |
||||||
|
ID string `json:"oid"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SocialAzureAD) Type() int { |
||||||
|
return int(models.AZUREAD) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SocialAzureAD) IsEmailAllowed(email string) bool { |
||||||
|
return isEmailAllowed(email, s.allowedDomains) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SocialAzureAD) IsSignupAllowed() bool { |
||||||
|
return s.allowSignup |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SocialAzureAD) UserInfo(_ *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { |
||||||
|
idToken := token.Extra("id_token") |
||||||
|
if idToken == nil { |
||||||
|
return nil, fmt.Errorf("No id_token found") |
||||||
|
} |
||||||
|
|
||||||
|
parsedToken, err := jwt.ParseSigned(idToken.(string)) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("Error parsing id token") |
||||||
|
} |
||||||
|
|
||||||
|
var claims azureClaims |
||||||
|
if err := parsedToken.UnsafeClaimsWithoutVerification(&claims); err != nil { |
||||||
|
return nil, fmt.Errorf("Error getting claims from id token") |
||||||
|
} |
||||||
|
|
||||||
|
email := extractEmail(claims) |
||||||
|
|
||||||
|
if email == "" { |
||||||
|
return nil, errors.New("Error getting user info: No email found in access token") |
||||||
|
} |
||||||
|
|
||||||
|
role := extractRole(claims) |
||||||
|
|
||||||
|
return &BasicUserInfo{ |
||||||
|
Id: claims.ID, |
||||||
|
Name: claims.Name, |
||||||
|
Email: email, |
||||||
|
Login: email, |
||||||
|
Role: string(role), |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func extractEmail(claims azureClaims) string { |
||||||
|
if claims.Email == "" { |
||||||
|
if claims.PreferredUsername != "" { |
||||||
|
return claims.PreferredUsername |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return claims.Email |
||||||
|
} |
||||||
|
|
||||||
|
func extractRole(claims azureClaims) models.RoleType { |
||||||
|
if len(claims.Roles) == 0 { |
||||||
|
return models.ROLE_VIEWER |
||||||
|
} |
||||||
|
|
||||||
|
roleOrder := []models.RoleType{ |
||||||
|
models.ROLE_ADMIN, |
||||||
|
models.ROLE_EDITOR, |
||||||
|
models.ROLE_VIEWER, |
||||||
|
} |
||||||
|
|
||||||
|
for _, role := range roleOrder { |
||||||
|
if found := hasRole(claims.Roles, role); found { |
||||||
|
return role |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return models.ROLE_VIEWER |
||||||
|
} |
||||||
|
|
||||||
|
func hasRole(roles []string, role models.RoleType) bool { |
||||||
|
for _, item := range roles { |
||||||
|
if strings.EqualFold(item, string(role)) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
@ -0,0 +1,234 @@ |
|||||||
|
package social |
||||||
|
|
||||||
|
import ( |
||||||
|
"golang.org/x/oauth2" |
||||||
|
"gopkg.in/square/go-jose.v2" |
||||||
|
"gopkg.in/square/go-jose.v2/jwt" |
||||||
|
"net/http" |
||||||
|
"reflect" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func TestSocialAzureAD_UserInfo(t *testing.T) { |
||||||
|
type fields struct { |
||||||
|
SocialBase *SocialBase |
||||||
|
allowedDomains []string |
||||||
|
allowSignup bool |
||||||
|
} |
||||||
|
type args struct { |
||||||
|
client *http.Client |
||||||
|
} |
||||||
|
|
||||||
|
tests := []struct { |
||||||
|
name string |
||||||
|
fields fields |
||||||
|
claims *azureClaims |
||||||
|
args args |
||||||
|
want *BasicUserInfo |
||||||
|
wantErr bool |
||||||
|
}{ |
||||||
|
{ |
||||||
|
name: "Email in email claim", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "me@example.com", |
||||||
|
PreferredUsername: "", |
||||||
|
Roles: []string{}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: &BasicUserInfo{ |
||||||
|
Id: "1234", |
||||||
|
Name: "My Name", |
||||||
|
Email: "me@example.com", |
||||||
|
Login: "me@example.com", |
||||||
|
Company: "", |
||||||
|
Role: "Viewer", |
||||||
|
Groups: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "No email", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "", |
||||||
|
PreferredUsername: "", |
||||||
|
Roles: []string{}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: nil, |
||||||
|
wantErr: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "No id token", |
||||||
|
claims: nil, |
||||||
|
want: nil, |
||||||
|
wantErr: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Email in preferred_username claim", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "", |
||||||
|
PreferredUsername: "me@example.com", |
||||||
|
Roles: []string{}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: &BasicUserInfo{ |
||||||
|
Id: "1234", |
||||||
|
Name: "My Name", |
||||||
|
Email: "me@example.com", |
||||||
|
Login: "me@example.com", |
||||||
|
Company: "", |
||||||
|
Role: "Viewer", |
||||||
|
Groups: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Admin role", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "me@example.com", |
||||||
|
PreferredUsername: "", |
||||||
|
Roles: []string{"Admin"}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: &BasicUserInfo{ |
||||||
|
Id: "1234", |
||||||
|
Name: "My Name", |
||||||
|
Email: "me@example.com", |
||||||
|
Login: "me@example.com", |
||||||
|
Company: "", |
||||||
|
Role: "Admin", |
||||||
|
Groups: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Lowercase Admin role", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "me@example.com", |
||||||
|
PreferredUsername: "", |
||||||
|
Roles: []string{"admin"}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: &BasicUserInfo{ |
||||||
|
Id: "1234", |
||||||
|
Name: "My Name", |
||||||
|
Email: "me@example.com", |
||||||
|
Login: "me@example.com", |
||||||
|
Company: "", |
||||||
|
Role: "Admin", |
||||||
|
Groups: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Only other roles", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "me@example.com", |
||||||
|
PreferredUsername: "", |
||||||
|
Roles: []string{"AppAdmin"}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: &BasicUserInfo{ |
||||||
|
Id: "1234", |
||||||
|
Name: "My Name", |
||||||
|
Email: "me@example.com", |
||||||
|
Login: "me@example.com", |
||||||
|
Company: "", |
||||||
|
Role: "Viewer", |
||||||
|
Groups: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
{ |
||||||
|
name: "Editor role", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "me@example.com", |
||||||
|
PreferredUsername: "", |
||||||
|
Roles: []string{"Editor"}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: &BasicUserInfo{ |
||||||
|
Id: "1234", |
||||||
|
Name: "My Name", |
||||||
|
Email: "me@example.com", |
||||||
|
Login: "me@example.com", |
||||||
|
Company: "", |
||||||
|
Role: "Editor", |
||||||
|
Groups: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Admin and Editor roles in claim", |
||||||
|
claims: &azureClaims{ |
||||||
|
Email: "me@example.com", |
||||||
|
PreferredUsername: "", |
||||||
|
Roles: []string{"Admin", "Editor"}, |
||||||
|
Name: "My Name", |
||||||
|
ID: "1234", |
||||||
|
}, |
||||||
|
want: &BasicUserInfo{ |
||||||
|
Id: "1234", |
||||||
|
Name: "My Name", |
||||||
|
Email: "me@example.com", |
||||||
|
Login: "me@example.com", |
||||||
|
Company: "", |
||||||
|
Role: "Admin", |
||||||
|
Groups: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
for _, tt := range tests { |
||||||
|
t.Run(tt.name, func(t *testing.T) { |
||||||
|
s := &SocialAzureAD{ |
||||||
|
SocialBase: tt.fields.SocialBase, |
||||||
|
allowedDomains: tt.fields.allowedDomains, |
||||||
|
allowSignup: tt.fields.allowSignup, |
||||||
|
} |
||||||
|
|
||||||
|
key := []byte("secret") |
||||||
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
cl := jwt.Claims{ |
||||||
|
Subject: "subject", |
||||||
|
Issuer: "issuer", |
||||||
|
NotBefore: jwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)), |
||||||
|
Audience: jwt.Audience{"leela", "fry"}, |
||||||
|
} |
||||||
|
|
||||||
|
var raw string |
||||||
|
if tt.claims != nil { |
||||||
|
raw, err = jwt.Signed(sig).Claims(cl).Claims(tt.claims).CompactSerialize() |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
raw, err = jwt.Signed(sig).Claims(cl).CompactSerialize() |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
token := &oauth2.Token{} |
||||||
|
if tt.claims != nil { |
||||||
|
token = token.WithExtra(map[string]interface{}{"id_token": raw}) |
||||||
|
} |
||||||
|
|
||||||
|
got, err := s.UserInfo(tt.args.client, token) |
||||||
|
if (err != nil) != tt.wantErr { |
||||||
|
t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr) |
||||||
|
return |
||||||
|
} |
||||||
|
if !reflect.DeepEqual(got, tt.want) { |
||||||
|
t.Errorf("UserInfo() got = %v, want %v", got, tt.want) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
After Width: | Height: | Size: 343 B |
@ -0,0 +1,334 @@ |
|||||||
|
/*- |
||||||
|
* Copyright 2016 Zbigniew Mandziejewicz |
||||||
|
* Copyright 2016 Square, Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package jwt |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"reflect" |
||||||
|
|
||||||
|
"gopkg.in/square/go-jose.v2/json" |
||||||
|
|
||||||
|
"gopkg.in/square/go-jose.v2" |
||||||
|
) |
||||||
|
|
||||||
|
// Builder is a utility for making JSON Web Tokens. Calls can be chained, and
|
||||||
|
// errors are accumulated until the final call to CompactSerialize/FullSerialize.
|
||||||
|
type Builder interface { |
||||||
|
// Claims encodes claims into JWE/JWS form. Multiple calls will merge claims
|
||||||
|
// into single JSON object. If you are passing private claims, make sure to set
|
||||||
|
// struct field tags to specify the name for the JSON key to be used when
|
||||||
|
// serializing.
|
||||||
|
Claims(i interface{}) Builder |
||||||
|
// Token builds a JSONWebToken from provided data.
|
||||||
|
Token() (*JSONWebToken, error) |
||||||
|
// FullSerialize serializes a token using the full serialization format.
|
||||||
|
FullSerialize() (string, error) |
||||||
|
// CompactSerialize serializes a token using the compact serialization format.
|
||||||
|
CompactSerialize() (string, error) |
||||||
|
} |
||||||
|
|
||||||
|
// NestedBuilder is a utility for making Signed-Then-Encrypted JSON Web Tokens.
|
||||||
|
// Calls can be chained, and errors are accumulated until final call to
|
||||||
|
// CompactSerialize/FullSerialize.
|
||||||
|
type NestedBuilder interface { |
||||||
|
// Claims encodes claims into JWE/JWS form. Multiple calls will merge claims
|
||||||
|
// into single JSON object. If you are passing private claims, make sure to set
|
||||||
|
// struct field tags to specify the name for the JSON key to be used when
|
||||||
|
// serializing.
|
||||||
|
Claims(i interface{}) NestedBuilder |
||||||
|
// Token builds a NestedJSONWebToken from provided data.
|
||||||
|
Token() (*NestedJSONWebToken, error) |
||||||
|
// FullSerialize serializes a token using the full serialization format.
|
||||||
|
FullSerialize() (string, error) |
||||||
|
// CompactSerialize serializes a token using the compact serialization format.
|
||||||
|
CompactSerialize() (string, error) |
||||||
|
} |
||||||
|
|
||||||
|
type builder struct { |
||||||
|
payload map[string]interface{} |
||||||
|
err error |
||||||
|
} |
||||||
|
|
||||||
|
type signedBuilder struct { |
||||||
|
builder |
||||||
|
sig jose.Signer |
||||||
|
} |
||||||
|
|
||||||
|
type encryptedBuilder struct { |
||||||
|
builder |
||||||
|
enc jose.Encrypter |
||||||
|
} |
||||||
|
|
||||||
|
type nestedBuilder struct { |
||||||
|
builder |
||||||
|
sig jose.Signer |
||||||
|
enc jose.Encrypter |
||||||
|
} |
||||||
|
|
||||||
|
// Signed creates builder for signed tokens.
|
||||||
|
func Signed(sig jose.Signer) Builder { |
||||||
|
return &signedBuilder{ |
||||||
|
sig: sig, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Encrypted creates builder for encrypted tokens.
|
||||||
|
func Encrypted(enc jose.Encrypter) Builder { |
||||||
|
return &encryptedBuilder{ |
||||||
|
enc: enc, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SignedAndEncrypted creates builder for signed-then-encrypted tokens.
|
||||||
|
// ErrInvalidContentType will be returned if encrypter doesn't have JWT content type.
|
||||||
|
func SignedAndEncrypted(sig jose.Signer, enc jose.Encrypter) NestedBuilder { |
||||||
|
if contentType, _ := enc.Options().ExtraHeaders[jose.HeaderContentType].(jose.ContentType); contentType != "JWT" { |
||||||
|
return &nestedBuilder{ |
||||||
|
builder: builder{ |
||||||
|
err: ErrInvalidContentType, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
return &nestedBuilder{ |
||||||
|
sig: sig, |
||||||
|
enc: enc, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (b builder) claims(i interface{}) builder { |
||||||
|
if b.err != nil { |
||||||
|
return b |
||||||
|
} |
||||||
|
|
||||||
|
m, ok := i.(map[string]interface{}) |
||||||
|
switch { |
||||||
|
case ok: |
||||||
|
return b.merge(m) |
||||||
|
case reflect.Indirect(reflect.ValueOf(i)).Kind() == reflect.Struct: |
||||||
|
m, err := normalize(i) |
||||||
|
if err != nil { |
||||||
|
return builder{ |
||||||
|
err: err, |
||||||
|
} |
||||||
|
} |
||||||
|
return b.merge(m) |
||||||
|
default: |
||||||
|
return builder{ |
||||||
|
err: ErrInvalidClaims, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func normalize(i interface{}) (map[string]interface{}, error) { |
||||||
|
m := make(map[string]interface{}) |
||||||
|
|
||||||
|
raw, err := json.Marshal(i) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
d := json.NewDecoder(bytes.NewReader(raw)) |
||||||
|
d.UseNumber() |
||||||
|
|
||||||
|
if err := d.Decode(&m); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return m, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *builder) merge(m map[string]interface{}) builder { |
||||||
|
p := make(map[string]interface{}) |
||||||
|
for k, v := range b.payload { |
||||||
|
p[k] = v |
||||||
|
} |
||||||
|
for k, v := range m { |
||||||
|
p[k] = v |
||||||
|
} |
||||||
|
|
||||||
|
return builder{ |
||||||
|
payload: p, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (b *builder) token(p func(interface{}) ([]byte, error), h []jose.Header) (*JSONWebToken, error) { |
||||||
|
return &JSONWebToken{ |
||||||
|
payload: p, |
||||||
|
Headers: h, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *signedBuilder) Claims(i interface{}) Builder { |
||||||
|
return &signedBuilder{ |
||||||
|
builder: b.builder.claims(i), |
||||||
|
sig: b.sig, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (b *signedBuilder) Token() (*JSONWebToken, error) { |
||||||
|
sig, err := b.sign() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
h := make([]jose.Header, len(sig.Signatures)) |
||||||
|
for i, v := range sig.Signatures { |
||||||
|
h[i] = v.Header |
||||||
|
} |
||||||
|
|
||||||
|
return b.builder.token(sig.Verify, h) |
||||||
|
} |
||||||
|
|
||||||
|
func (b *signedBuilder) CompactSerialize() (string, error) { |
||||||
|
sig, err := b.sign() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return sig.CompactSerialize() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *signedBuilder) FullSerialize() (string, error) { |
||||||
|
sig, err := b.sign() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return sig.FullSerialize(), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *signedBuilder) sign() (*jose.JSONWebSignature, error) { |
||||||
|
if b.err != nil { |
||||||
|
return nil, b.err |
||||||
|
} |
||||||
|
|
||||||
|
p, err := json.Marshal(b.payload) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return b.sig.Sign(p) |
||||||
|
} |
||||||
|
|
||||||
|
func (b *encryptedBuilder) Claims(i interface{}) Builder { |
||||||
|
return &encryptedBuilder{ |
||||||
|
builder: b.builder.claims(i), |
||||||
|
enc: b.enc, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (b *encryptedBuilder) CompactSerialize() (string, error) { |
||||||
|
enc, err := b.encrypt() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return enc.CompactSerialize() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *encryptedBuilder) FullSerialize() (string, error) { |
||||||
|
enc, err := b.encrypt() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return enc.FullSerialize(), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *encryptedBuilder) Token() (*JSONWebToken, error) { |
||||||
|
enc, err := b.encrypt() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return b.builder.token(enc.Decrypt, []jose.Header{enc.Header}) |
||||||
|
} |
||||||
|
|
||||||
|
func (b *encryptedBuilder) encrypt() (*jose.JSONWebEncryption, error) { |
||||||
|
if b.err != nil { |
||||||
|
return nil, b.err |
||||||
|
} |
||||||
|
|
||||||
|
p, err := json.Marshal(b.payload) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return b.enc.Encrypt(p) |
||||||
|
} |
||||||
|
|
||||||
|
func (b *nestedBuilder) Claims(i interface{}) NestedBuilder { |
||||||
|
return &nestedBuilder{ |
||||||
|
builder: b.builder.claims(i), |
||||||
|
sig: b.sig, |
||||||
|
enc: b.enc, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (b *nestedBuilder) Token() (*NestedJSONWebToken, error) { |
||||||
|
enc, err := b.signAndEncrypt() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &NestedJSONWebToken{ |
||||||
|
enc: enc, |
||||||
|
Headers: []jose.Header{enc.Header}, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *nestedBuilder) CompactSerialize() (string, error) { |
||||||
|
enc, err := b.signAndEncrypt() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return enc.CompactSerialize() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *nestedBuilder) FullSerialize() (string, error) { |
||||||
|
enc, err := b.signAndEncrypt() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return enc.FullSerialize(), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *nestedBuilder) signAndEncrypt() (*jose.JSONWebEncryption, error) { |
||||||
|
if b.err != nil { |
||||||
|
return nil, b.err |
||||||
|
} |
||||||
|
|
||||||
|
p, err := json.Marshal(b.payload) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
sig, err := b.sig.Sign(p) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
p2, err := sig.CompactSerialize() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return b.enc.Encrypt([]byte(p2)) |
||||||
|
} |
||||||
@ -0,0 +1,120 @@ |
|||||||
|
/*- |
||||||
|
* Copyright 2016 Zbigniew Mandziejewicz |
||||||
|
* Copyright 2016 Square, Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package jwt |
||||||
|
|
||||||
|
import ( |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
|
||||||
|
"gopkg.in/square/go-jose.v2/json" |
||||||
|
) |
||||||
|
|
||||||
|
// Claims represents public claim values (as specified in RFC 7519).
|
||||||
|
type Claims struct { |
||||||
|
Issuer string `json:"iss,omitempty"` |
||||||
|
Subject string `json:"sub,omitempty"` |
||||||
|
Audience Audience `json:"aud,omitempty"` |
||||||
|
Expiry *NumericDate `json:"exp,omitempty"` |
||||||
|
NotBefore *NumericDate `json:"nbf,omitempty"` |
||||||
|
IssuedAt *NumericDate `json:"iat,omitempty"` |
||||||
|
ID string `json:"jti,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// NumericDate represents date and time as the number of seconds since the
|
||||||
|
// epoch, including leap seconds. Non-integer values can be represented
|
||||||
|
// in the serialized format, but we round to the nearest second.
|
||||||
|
type NumericDate int64 |
||||||
|
|
||||||
|
// NewNumericDate constructs NumericDate from time.Time value.
|
||||||
|
func NewNumericDate(t time.Time) *NumericDate { |
||||||
|
if t.IsZero() { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// While RFC 7519 technically states that NumericDate values may be
|
||||||
|
// non-integer values, we don't bother serializing timestamps in
|
||||||
|
// claims with sub-second accurancy and just round to the nearest
|
||||||
|
// second instead. Not convined sub-second accuracy is useful here.
|
||||||
|
out := NumericDate(t.Unix()) |
||||||
|
return &out |
||||||
|
} |
||||||
|
|
||||||
|
// MarshalJSON serializes the given NumericDate into its JSON representation.
|
||||||
|
func (n NumericDate) MarshalJSON() ([]byte, error) { |
||||||
|
return []byte(strconv.FormatInt(int64(n), 10)), nil |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON reads a date from its JSON representation.
|
||||||
|
func (n *NumericDate) UnmarshalJSON(b []byte) error { |
||||||
|
s := string(b) |
||||||
|
|
||||||
|
f, err := strconv.ParseFloat(s, 64) |
||||||
|
if err != nil { |
||||||
|
return ErrUnmarshalNumericDate |
||||||
|
} |
||||||
|
|
||||||
|
*n = NumericDate(f) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Time returns time.Time representation of NumericDate.
|
||||||
|
func (n *NumericDate) Time() time.Time { |
||||||
|
if n == nil { |
||||||
|
return time.Time{} |
||||||
|
} |
||||||
|
return time.Unix(int64(*n), 0) |
||||||
|
} |
||||||
|
|
||||||
|
// Audience represents the recipents that the token is intended for.
|
||||||
|
type Audience []string |
||||||
|
|
||||||
|
// UnmarshalJSON reads an audience from its JSON representation.
|
||||||
|
func (s *Audience) UnmarshalJSON(b []byte) error { |
||||||
|
var v interface{} |
||||||
|
if err := json.Unmarshal(b, &v); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
switch v := v.(type) { |
||||||
|
case string: |
||||||
|
*s = []string{v} |
||||||
|
case []interface{}: |
||||||
|
a := make([]string, len(v)) |
||||||
|
for i, e := range v { |
||||||
|
s, ok := e.(string) |
||||||
|
if !ok { |
||||||
|
return ErrUnmarshalAudience |
||||||
|
} |
||||||
|
a[i] = s |
||||||
|
} |
||||||
|
*s = a |
||||||
|
default: |
||||||
|
return ErrUnmarshalAudience |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s Audience) Contains(v string) bool { |
||||||
|
for _, a := range s { |
||||||
|
if a == v { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
/*- |
||||||
|
* Copyright 2017 Square Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
/* |
||||||
|
|
||||||
|
Package jwt provides an implementation of the JSON Web Token standard. |
||||||
|
|
||||||
|
*/ |
||||||
|
package jwt |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
/*- |
||||||
|
* Copyright 2016 Zbigniew Mandziejewicz |
||||||
|
* Copyright 2016 Square, Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package jwt |
||||||
|
|
||||||
|
import "errors" |
||||||
|
|
||||||
|
// ErrUnmarshalAudience indicates that aud claim could not be unmarshalled.
|
||||||
|
var ErrUnmarshalAudience = errors.New("square/go-jose/jwt: expected string or array value to unmarshal to Audience") |
||||||
|
|
||||||
|
// ErrUnmarshalNumericDate indicates that JWT NumericDate could not be unmarshalled.
|
||||||
|
var ErrUnmarshalNumericDate = errors.New("square/go-jose/jwt: expected number value to unmarshal NumericDate") |
||||||
|
|
||||||
|
// ErrInvalidClaims indicates that given claims have invalid type.
|
||||||
|
var ErrInvalidClaims = errors.New("square/go-jose/jwt: expected claims to be value convertible into JSON object") |
||||||
|
|
||||||
|
// ErrInvalidIssuer indicates invalid iss claim.
|
||||||
|
var ErrInvalidIssuer = errors.New("square/go-jose/jwt: validation failed, invalid issuer claim (iss)") |
||||||
|
|
||||||
|
// ErrInvalidSubject indicates invalid sub claim.
|
||||||
|
var ErrInvalidSubject = errors.New("square/go-jose/jwt: validation failed, invalid subject claim (sub)") |
||||||
|
|
||||||
|
// ErrInvalidAudience indicated invalid aud claim.
|
||||||
|
var ErrInvalidAudience = errors.New("square/go-jose/jwt: validation failed, invalid audience claim (aud)") |
||||||
|
|
||||||
|
// ErrInvalidID indicates invalid jti claim.
|
||||||
|
var ErrInvalidID = errors.New("square/go-jose/jwt: validation failed, invalid ID claim (jti)") |
||||||
|
|
||||||
|
// ErrNotValidYet indicates that token is used before time indicated in nbf claim.
|
||||||
|
var ErrNotValidYet = errors.New("square/go-jose/jwt: validation failed, token not valid yet (nbf)") |
||||||
|
|
||||||
|
// ErrExpired indicates that token is used after expiry time indicated in exp claim.
|
||||||
|
var ErrExpired = errors.New("square/go-jose/jwt: validation failed, token is expired (exp)") |
||||||
|
|
||||||
|
// ErrIssuedInTheFuture indicates that the iat field is in the future.
|
||||||
|
var ErrIssuedInTheFuture = errors.New("square/go-jose/jwt: validation field, token issued in the future (iat)") |
||||||
|
|
||||||
|
// ErrInvalidContentType indicates that token requires JWT cty header.
|
||||||
|
var ErrInvalidContentType = errors.New("square/go-jose/jwt: expected content type to be JWT (cty header)") |
||||||
@ -0,0 +1,163 @@ |
|||||||
|
/*- |
||||||
|
* Copyright 2016 Zbigniew Mandziejewicz |
||||||
|
* Copyright 2016 Square, Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package jwt |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
jose "gopkg.in/square/go-jose.v2" |
||||||
|
"gopkg.in/square/go-jose.v2/json" |
||||||
|
) |
||||||
|
|
||||||
|
// JSONWebToken represents a JSON Web Token (as specified in RFC7519).
|
||||||
|
type JSONWebToken struct { |
||||||
|
payload func(k interface{}) ([]byte, error) |
||||||
|
unverifiedPayload func() []byte |
||||||
|
Headers []jose.Header |
||||||
|
} |
||||||
|
|
||||||
|
type NestedJSONWebToken struct { |
||||||
|
enc *jose.JSONWebEncryption |
||||||
|
Headers []jose.Header |
||||||
|
} |
||||||
|
|
||||||
|
// Claims deserializes a JSONWebToken into dest using the provided key.
|
||||||
|
func (t *JSONWebToken) Claims(key interface{}, dest ...interface{}) error { |
||||||
|
payloadKey := tryJWKS(t.Headers, key) |
||||||
|
|
||||||
|
b, err := t.payload(payloadKey) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
for _, d := range dest { |
||||||
|
if err := json.Unmarshal(b, d); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// UnsafeClaimsWithoutVerification deserializes the claims of a
|
||||||
|
// JSONWebToken into the dests. For signed JWTs, the claims are not
|
||||||
|
// verified. This function won't work for encrypted JWTs.
|
||||||
|
func (t *JSONWebToken) UnsafeClaimsWithoutVerification(dest ...interface{}) error { |
||||||
|
if t.unverifiedPayload == nil { |
||||||
|
return fmt.Errorf("square/go-jose: Cannot get unverified claims") |
||||||
|
} |
||||||
|
claims := t.unverifiedPayload() |
||||||
|
for _, d := range dest { |
||||||
|
if err := json.Unmarshal(claims, d); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken, error) { |
||||||
|
key := tryJWKS(t.Headers, decryptionKey) |
||||||
|
|
||||||
|
b, err := t.enc.Decrypt(key) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
sig, err := ParseSigned(string(b)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return sig, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseSigned parses token from JWS form.
|
||||||
|
func ParseSigned(s string) (*JSONWebToken, error) { |
||||||
|
sig, err := jose.ParseSigned(s) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
headers := make([]jose.Header, len(sig.Signatures)) |
||||||
|
for i, signature := range sig.Signatures { |
||||||
|
headers[i] = signature.Header |
||||||
|
} |
||||||
|
|
||||||
|
return &JSONWebToken{ |
||||||
|
payload: sig.Verify, |
||||||
|
unverifiedPayload: sig.UnsafePayloadWithoutVerification, |
||||||
|
Headers: headers, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseEncrypted parses token from JWE form.
|
||||||
|
func ParseEncrypted(s string) (*JSONWebToken, error) { |
||||||
|
enc, err := jose.ParseEncrypted(s) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &JSONWebToken{ |
||||||
|
payload: enc.Decrypt, |
||||||
|
Headers: []jose.Header{enc.Header}, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseSignedAndEncrypted parses signed-then-encrypted token from JWE form.
|
||||||
|
func ParseSignedAndEncrypted(s string) (*NestedJSONWebToken, error) { |
||||||
|
enc, err := jose.ParseEncrypted(s) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
contentType, _ := enc.Header.ExtraHeaders[jose.HeaderContentType].(string) |
||||||
|
if strings.ToUpper(contentType) != "JWT" { |
||||||
|
return nil, ErrInvalidContentType |
||||||
|
} |
||||||
|
|
||||||
|
return &NestedJSONWebToken{ |
||||||
|
enc: enc, |
||||||
|
Headers: []jose.Header{enc.Header}, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func tryJWKS(headers []jose.Header, key interface{}) interface{} { |
||||||
|
jwks, ok := key.(*jose.JSONWebKeySet) |
||||||
|
if !ok { |
||||||
|
return key |
||||||
|
} |
||||||
|
|
||||||
|
var kid string |
||||||
|
for _, header := range headers { |
||||||
|
if header.KeyID != "" { |
||||||
|
kid = header.KeyID |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if kid == "" { |
||||||
|
return key |
||||||
|
} |
||||||
|
|
||||||
|
keys := jwks.Key(kid) |
||||||
|
if len(keys) == 0 { |
||||||
|
return key |
||||||
|
} |
||||||
|
|
||||||
|
return keys[0].Key |
||||||
|
} |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
/*- |
||||||
|
* Copyright 2016 Zbigniew Mandziejewicz |
||||||
|
* Copyright 2016 Square, Inc. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package jwt |
||||||
|
|
||||||
|
import "time" |
||||||
|
|
||||||
|
const ( |
||||||
|
// DefaultLeeway defines the default leeway for matching NotBefore/Expiry claims.
|
||||||
|
DefaultLeeway = 1.0 * time.Minute |
||||||
|
) |
||||||
|
|
||||||
|
// Expected defines values used for protected claims validation.
|
||||||
|
// If field has zero value then validation is skipped.
|
||||||
|
type Expected struct { |
||||||
|
// Issuer matches the "iss" claim exactly.
|
||||||
|
Issuer string |
||||||
|
// Subject matches the "sub" claim exactly.
|
||||||
|
Subject string |
||||||
|
// Audience matches the values in "aud" claim, regardless of their order.
|
||||||
|
Audience Audience |
||||||
|
// ID matches the "jti" claim exactly.
|
||||||
|
ID string |
||||||
|
// Time matches the "exp" and "nbf" claims with leeway.
|
||||||
|
Time time.Time |
||||||
|
} |
||||||
|
|
||||||
|
// WithTime copies expectations with new time.
|
||||||
|
func (e Expected) WithTime(t time.Time) Expected { |
||||||
|
e.Time = t |
||||||
|
return e |
||||||
|
} |
||||||
|
|
||||||
|
// Validate checks claims in a token against expected values.
|
||||||
|
// A default leeway value of one minute is used to compare time values.
|
||||||
|
//
|
||||||
|
// The default leeway will cause the token to be deemed valid until one
|
||||||
|
// minute after the expiration time. If you're a server application that
|
||||||
|
// wants to give an extra minute to client tokens, use this
|
||||||
|
// function. If you're a client application wondering if the server
|
||||||
|
// will accept your token, use ValidateWithLeeway with a leeway <=0,
|
||||||
|
// otherwise this function might make you think a token is valid when
|
||||||
|
// it is not.
|
||||||
|
func (c Claims) Validate(e Expected) error { |
||||||
|
return c.ValidateWithLeeway(e, DefaultLeeway) |
||||||
|
} |
||||||
|
|
||||||
|
// ValidateWithLeeway checks claims in a token against expected values. A
|
||||||
|
// custom leeway may be specified for comparing time values. You may pass a
|
||||||
|
// zero value to check time values with no leeway, but you should not that
|
||||||
|
// numeric date values are rounded to the nearest second and sub-second
|
||||||
|
// precision is not supported.
|
||||||
|
//
|
||||||
|
// The leeway gives some extra time to the token from the server's
|
||||||
|
// point of view. That is, if the token is expired, ValidateWithLeeway
|
||||||
|
// will still accept the token for 'leeway' amount of time. This fails
|
||||||
|
// if you're using this function to check if a server will accept your
|
||||||
|
// token, because it will think the token is valid even after it
|
||||||
|
// expires. So if you're a client validating if the token is valid to
|
||||||
|
// be submitted to a server, use leeway <=0, if you're a server
|
||||||
|
// validation a token, use leeway >=0.
|
||||||
|
func (c Claims) ValidateWithLeeway(e Expected, leeway time.Duration) error { |
||||||
|
if e.Issuer != "" && e.Issuer != c.Issuer { |
||||||
|
return ErrInvalidIssuer |
||||||
|
} |
||||||
|
|
||||||
|
if e.Subject != "" && e.Subject != c.Subject { |
||||||
|
return ErrInvalidSubject |
||||||
|
} |
||||||
|
|
||||||
|
if e.ID != "" && e.ID != c.ID { |
||||||
|
return ErrInvalidID |
||||||
|
} |
||||||
|
|
||||||
|
if len(e.Audience) != 0 { |
||||||
|
for _, v := range e.Audience { |
||||||
|
if !c.Audience.Contains(v) { |
||||||
|
return ErrInvalidAudience |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if !e.Time.IsZero() { |
||||||
|
if c.NotBefore != nil && e.Time.Add(leeway).Before(c.NotBefore.Time()) { |
||||||
|
return ErrNotValidYet |
||||||
|
} |
||||||
|
|
||||||
|
if c.Expiry != nil && e.Time.Add(-leeway).After(c.Expiry.Time()) { |
||||||
|
return ErrExpired |
||||||
|
} |
||||||
|
|
||||||
|
// IssuedAt is optional but cannot be in the future. This is not required by the RFC, but
|
||||||
|
// something is misconfigured if this happens and we should not trust it.
|
||||||
|
if c.IssuedAt != nil && e.Time.Add(leeway).Before(c.IssuedAt.Time()) { |
||||||
|
return ErrIssuedInTheFuture |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
Loading…
Reference in new issue