Skip to content

Commit d7afaac

Browse files
committed
oauth2: initial implementation of OAuth 2.0 Dynamic Client Registration Protocol
There is no OAuth 2.0 Dynamic Client Registration Protocol implementation available in oauth2. The protocol is a PROPOSED STANDARD, however, many OAuth servers already support it. Dynamic Client Registration Protocol is described at https://tools.ietf.org/html/rfc7591
1 parent bf48bf1 commit d7afaac

File tree

2 files changed

+436
-0
lines changed

2 files changed

+436
-0
lines changed

dcrp/dcrp.go

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// Copyright 2014 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package dcrp implements the OAuth 2.0 Dynamic Client Registration Protocol.
6+
// This specification defines mechanisms for dynamically registering OAuth 2.0 clients with authorization servers.
7+
//
8+
// See https://tools.ietf.org/html/rfc7591
9+
10+
package dcrp // import "golang.org/x/oauth2/dcrp"
11+
12+
import (
13+
"bytes"
14+
"encoding/json"
15+
"errors"
16+
"fmt"
17+
"io/ioutil"
18+
"net/http"
19+
"strings"
20+
"time"
21+
)
22+
23+
// Config describes Dynamic Client Registration configuration
24+
type Config struct {
25+
// InitialAccessToken specifies access token used to get access to get access to
26+
// client registration endpoint URL. The method by which the initial access token
27+
// is obtained by the client or developer is generally out of band
28+
InitialAccessToken string
29+
30+
// ClientRegistrationEndpointURL specifies authorization server's client registration endpoint URL
31+
// This is a constant specific to each server.
32+
ClientRegistrationEndpointURL string
33+
34+
// Metadata specifies client metadata to be used for client registration
35+
Metadata
36+
}
37+
38+
// Metadata describes client metadata.
39+
// Registered clients have a set of metadata values associated with their
40+
// client identifier at an authorization server. The implementation
41+
// and use of all client metadata fields is OPTIONAL
42+
type Metadata struct {
43+
// RedirectURIs specifies redirection URI strings for use in
44+
// redirect-based flows such as the "authorization code" and "implicit".
45+
RedirectURIs []string `json:"redirect_uris,omitempty"`
46+
47+
// TokenEndpointAuthMethod specifies indicator of the requested authentication
48+
// method for the token endpoint
49+
// Possible values are:
50+
// "none": The client is a public client and does not have a client secret.
51+
// "client_secret_post": The client uses the HTTP POST parameters
52+
// "client_secret_basic": The client uses HTTP Basic
53+
// Additional values can be defined or absolute URIs can also be used
54+
// as values for this parameter without being registered.
55+
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
56+
57+
// GrantTypes specifies grant type strings that the client can use at the token endpoint
58+
// Possible values are:
59+
// "authorization_code": The authorization code grant type
60+
// "implicit": The implicit grant type
61+
// "password": The resource owner password credentials grant type
62+
// "client_credentials": The client credentials grant type
63+
// "refresh_token": The refresh token grant type
64+
// "urn:ietf:params:oauth:grant-type:jwt-bearer": The JWT Bearer Token Grant Type
65+
// "urn:ietf:params:oauth:grant-type:saml2-bearer": The SAML 2.0 Bearer Assertion Grant
66+
GrantTypes []string `json:"grant_types,omitempty"`
67+
68+
// ResponseTypes specifies response type strings that the client can
69+
// use at the authorization endpoint.
70+
// Possible values are:
71+
// "code": The "authorization code" response
72+
// "token": The "implicit" response
73+
ResponseTypes []string `json:"response_types,omitempty"`
74+
75+
// ClientName specifies Human-readable string name of the client
76+
// to be presented to the end-user during authorization
77+
ClientName string `json:"client_name,omitempty"`
78+
79+
// ClientURI specifies URL of a web page providing information about the client.
80+
ClientURI string `json:"client_uri,omitempty"`
81+
82+
// LogoURI specifies URL of a logo of the client
83+
LogoURI string `json:"logo_uri,omitempty"`
84+
85+
// Scopes specifies scope values that the client can use when requesting access tokens.
86+
Scopes []string `json:"-"`
87+
88+
// Scope specifies wire-level scopes representation
89+
Scope string `json:"scope,omitempty"`
90+
91+
// Contacts specifies ways to contact people responsible for this client,
92+
// typically email addresses.
93+
Contacts []string `json:"contacts,omitempty"`
94+
95+
// TermsOfServiceURI specifies URL of a human-readable terms of service
96+
// document for the client
97+
TermsOfServiceURI string `json:"tos_uri,omitempty"`
98+
99+
// PolicyURI specifies URL of a human-readable privacy policy document
100+
PolicyURI string `json:"policy_uri,omitempty"`
101+
102+
// JWKSURI specifies URL referencing the client's JWK Set [RFC7517] document,
103+
// which contains the client's public keys.
104+
JWKSURI string `json:"jwks_uri,omitempty"`
105+
106+
// JWKS specifies the client's JWK Set [RFC7517] document, which contains
107+
// the client's public keys. The value of this field MUST be a JSON
108+
// containing a valid JWK Set.
109+
JWKS string `json:"jwks,omitempty"`
110+
111+
// SoftwareID specifies UUID assigned by the client developer or software publisher
112+
// used by registration endpoints to identify the client software.
113+
SoftwareID string `json:"software_id,omitempty"`
114+
115+
// SoftwareVersion specifies version of the client software
116+
SoftwareVersion string `json:"software_version,omitempty"`
117+
118+
// SoftwareStatement specifies client metadata values about the client software
119+
// as claims. This is a string value containing the entire signed JWT.
120+
SoftwareStatement string `json:"software_statement,omitempty"`
121+
122+
// Optional specifies optional fields
123+
Optional map[string]string `json:"-"`
124+
}
125+
126+
// prepareForWire prepares Metadata struct to be ready to sent to server.
127+
func (md *Metadata) prepareForWire() {
128+
md.Scope = strings.Join(md.Scopes, " ")
129+
}
130+
131+
// prepareFromWire prepares Metadata to be ready to be used by user
132+
func (md *Metadata) prepareFromWire() {
133+
md.Scopes = strings.Split(md.Scope, " ")
134+
}
135+
136+
// Response describes Client Information Response as specified in Section 3.2.1 of RFC 7591
137+
type Response struct {
138+
// ClientID specifies client identifier string. REQUIRED
139+
ClientID string `json:"client_id"`
140+
141+
// ClientSecret specifies client secret string. OPTIONAL
142+
ClientSecret string `json:"client_secret"`
143+
144+
// ClientIDIssuedAt specifies time at which the client identifier was issued. OPTIONAL
145+
ClientIDIssuedAt time.Time `json:"client_id_issued_at"`
146+
147+
// ClientSecretExpiresAt specifies time at which the client secret will expire
148+
// or 0 if it will not expire. REQUIRED if "client_secret" is issued.
149+
ClientSecretExpiresAt time.Time `json:"client_secret_expires_at"`
150+
151+
// Additionally, the authorization server MUST return all registered metadata about this client
152+
Metadata `json:",inline"`
153+
}
154+
155+
// Register performs Dynamic Client Registration dy doing round trip to authorization server
156+
func (c *Config) Register() (*Response, error) {
157+
c.Metadata.prepareForWire()
158+
jsonMetadata, err := json.Marshal(c.Metadata)
159+
if err != nil {
160+
return nil, err
161+
}
162+
req, err := newHTTPRequest(c.ClientRegistrationEndpointURL, c.InitialAccessToken, jsonMetadata)
163+
if err != nil {
164+
return nil, err
165+
}
166+
return doRoundTrip(req)
167+
}
168+
169+
// RegistrationError describes errors returned by auth server during client registration process
170+
type RegistrationError struct {
171+
Response *http.Response
172+
Body []byte
173+
}
174+
175+
func (r *RegistrationError) Error() string {
176+
return fmt.Sprintf("oauth2: cannot register client: %v\nResponse: %s", r.Response.Status, r.Body)
177+
}
178+
179+
// newHTTPRequest returns a new *http.Request to be used for client registration
180+
// It has header fields specified
181+
func newHTTPRequest(registrationURL, initialAccessToken string, body []byte) (*http.Request, error) {
182+
req, err := http.NewRequest("POST", registrationURL, bytes.NewBuffer(body))
183+
if err != nil {
184+
return nil, err
185+
}
186+
req.Header.Set("Content-Type", "application/json")
187+
if initialAccessToken != "" {
188+
req.Header.Set("Authorization", "Bearer "+initialAccessToken)
189+
}
190+
return req, nil
191+
}
192+
193+
// doRoundTrip performs communication with authorization server for client registration
194+
func doRoundTrip(req *http.Request) (*Response, error) {
195+
client := http.Client{}
196+
resp, err := client.Do(req)
197+
if err != nil {
198+
return nil, err
199+
}
200+
defer resp.Body.Close()
201+
202+
body, err := ioutil.ReadAll(resp.Body)
203+
if err != nil {
204+
return nil, fmt.Errorf("oauth2 dcrp: cannot read server response: %v", err)
205+
}
206+
// The server responds with an HTTP 201 Created status code and a body of type "application/json"
207+
if code := resp.StatusCode; code != 201 {
208+
return nil, &RegistrationError{
209+
Response: resp,
210+
Body: body,
211+
}
212+
}
213+
214+
// The response contains the client identifier as well as the client secret,
215+
// if the client is a confidential client.
216+
// The response MAY contain additional fields
217+
cr := &Response{}
218+
if err = json.Unmarshal(body, cr); err != nil {
219+
return nil, err
220+
}
221+
cr.Metadata.prepareFromWire()
222+
if cr.ClientID == "" {
223+
return nil, errors.New("oauth2 dcrp: server response missing required client_id in body:\n" + string(body))
224+
}
225+
return cr, nil
226+
}
227+
228+
// MarshalJSON prepares Response for wire JSON representation
229+
func (r Response) MarshalJSON() ([]byte, error) {
230+
type Alias Response
231+
wire := struct {
232+
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
233+
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"`
234+
Alias
235+
}{
236+
ClientIDIssuedAt: r.ClientIDIssuedAt.Unix(),
237+
ClientSecretExpiresAt: r.ClientSecretExpiresAt.Unix(),
238+
Alias: (Alias)(r),
239+
}
240+
return json.Marshal(wire)
241+
}
242+
243+
// MarshalJSON prepares Response from wire JSON representation
244+
func (r *Response) UnmarshalJSON(data []byte) error {
245+
type Alias Response
246+
wire := &struct {
247+
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
248+
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"`
249+
*Alias
250+
}{
251+
Alias: (*Alias)(r),
252+
}
253+
if err := json.Unmarshal(data, &wire); err != nil {
254+
return err
255+
}
256+
r.ClientIDIssuedAt = time.Unix(wire.ClientIDIssuedAt, 0)
257+
r.ClientSecretExpiresAt = time.Unix(wire.ClientSecretExpiresAt, 0)
258+
return nil
259+
}

0 commit comments

Comments
 (0)