1 /* 2 * Copyright © 2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io> 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 * @author Aeneas Rekkas <aeneas+oss@aeneas.io> 17 * @copyright 2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io> 18 * @license Apache-2.0 19 * 20 */ 21 22 package fosite 23 24 import ( 25 "encoding/json" 26 "net/http" 27 "strings" 28 29 "github.com/pkg/errors" 30 ) 31 32 // WriteIntrospectionError responds with token metadata discovered by token introspection as defined in 33 // https://tools.ietf.org/search/rfc7662#section-2.2 34 // 35 // If the protected resource uses OAuth 2.0 client credentials to 36 // authenticate to the introspection endpoint and its credentials are 37 // invalid, the authorization server responds with an HTTP 401 38 // (Unauthorized) as described in Section 5.2 of OAuth 2.0 [RFC6749]. 39 // 40 // If the protected resource uses an OAuth 2.0 bearer token to authorize 41 // its call to the introspection endpoint and the token used for 42 // authorization does not contain sufficient privileges or is otherwise 43 // invalid for this request, the authorization server responds with an 44 // HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token 45 // Usage [RFC6750]. 46 // 47 // Note that a properly formed and authorized query for an inactive or 48 // otherwise invalid token (or a token the protected resource is not 49 // allowed to know about) is not considered an error response by this 50 // specification. In these cases, the authorization server MUST instead 51 // respond with an introspection response with the "active" field set to 52 // "false" as described in Section 2.2. 53 func (f *Fosite) WriteIntrospectionError(rw http.ResponseWriter, err error) { 54 if err == nil { 55 return 56 } 57 58 // Inactive token errors should never written out as an error. 59 if !errors.Is(err, ErrInactiveToken) && (errors.Is(err, ErrInvalidRequest) || errors.Is(err, ErrRequestUnauthorized)) { 60 f.writeJsonError(rw, nil, err) 61 return 62 } 63 64 rw.Header().Set("Content-Type", "application/json;charset=UTF-8") 65 rw.Header().Set("Cache-Control", "no-store") 66 rw.Header().Set("Pragma", "no-cache") 67 _ = json.NewEncoder(rw).Encode(struct { 68 Active bool `json:"active"` 69 }{Active: false}) 70 } 71 72 // WriteIntrospectionResponse responds with an error if token introspection failed as defined in 73 // https://tools.ietf.org/search/rfc7662#section-2.3 74 // 75 // The server responds with a JSON object [RFC7159] in "application/ 76 // json" format with the following top-level members. 77 // 78 // * active 79 // REQUIRED. Boolean indicator of whether or not the presented token 80 // is currently active. The specifics of a token's "active" state 81 // will vary depending on the implementation of the authorization 82 // server and the information it keeps about its tokens, but a "true" 83 // value return for the "active" property will generally indicate 84 // that a given token has been issued by this authorization server, 85 // has not been revoked by the resource owner, and is within its 86 // given time window of validity (e.g., after its issuance time and 87 // before its expiration time). See Section 4 for information on 88 // implementation of such checks. 89 // 90 // * scope 91 // OPTIONAL. A JSON string containing a space-separated list of 92 // scopes associated with this token, in the format described in 93 // Section 3.3 of OAuth 2.0 [RFC6749]. 94 // 95 // * client_id 96 // OPTIONAL. Client identifier for the OAuth 2.0 client that 97 // requested this token. 98 // 99 // * username 100 // OPTIONAL. Human-readable identifier for the resource owner who 101 // authorized this token. 102 // 103 // * token_type 104 // OPTIONAL. Type of the token as defined in Section 5.1 of OAuth 105 // 2.0 [RFC6749]. 106 // 107 // * exp 108 // OPTIONAL. Integer timestamp, measured in the number of seconds 109 // since January 1 1970 UTC, indicating when this token will expire, 110 // as defined in JWT [RFC7519]. 111 // 112 // * iat 113 // OPTIONAL. Integer timestamp, measured in the number of seconds 114 // since January 1 1970 UTC, indicating when this token was 115 // originally issued, as defined in JWT [RFC7519]. 116 // 117 // * nbf 118 // OPTIONAL. Integer timestamp, measured in the number of seconds 119 // since January 1 1970 UTC, indicating when this token is not to be 120 // used before, as defined in JWT [RFC7519]. 121 // 122 // * sub 123 // OPTIONAL. Subject of the token, as defined in JWT [RFC7519]. 124 // Usually a machine-readable identifier of the resource owner who 125 // authorized this token. 126 // 127 // * aud 128 // OPTIONAL. Service-specific string identifier or list of string 129 // identifiers representing the intended audience for this token, as 130 // defined in JWT [RFC7519]. 131 // 132 // * iss 133 // OPTIONAL. String representing the issuer of this token, as 134 // defined in JWT [RFC7519]. 135 // 136 // * jti 137 // OPTIONAL. String identifier for the token, as defined in JWT 138 // [RFC7519]. 139 // 140 // Specific implementations MAY extend this structure with their own 141 // service-specific response names as top-level members of this JSON 142 // object. Response names intended to be used across domains MUST be 143 // registered in the "OAuth Token Introspection Response" registry 144 // defined in Section 3.1. 145 // 146 // The authorization server MAY respond differently to different 147 // protected resources making the same request. For instance, an 148 // authorization server MAY limit which scopes from a given token are 149 // returned for each protected resource to prevent a protected resource 150 // from learning more about the larger network than is necessary for its 151 // operation. 152 // 153 // The response MAY be cached by the protected resource to improve 154 // performance and reduce load on the introspection endpoint, but at the 155 // cost of liveness of the information used by the protected resource to 156 // make authorization decisions. See Section 4 for more information 157 // regarding the trade off when the response is cached. 158 // 159 // 160 // For example, the following response contains a set of information 161 // about an active token: 162 // 163 // The following is a non-normative example response: 164 // 165 // HTTP/1.1 200 OK 166 // Content-Type: application/json 167 // 168 // { 169 // "active": true, 170 // "client_id": "l238j323ds-23ij4", 171 // "username": "jdoe", 172 // "scope": "read write dolphin", 173 // "sub": "Z5O3upPC88QrAjx00dis", 174 // "aud": "https://protected.example.net/resource", 175 // "iss": "https://server.example.com/", 176 // "exp": 1419356238, 177 // "iat": 1419350238, 178 // "extension_field": "twenty-seven" 179 // } 180 // 181 // If the introspection call is properly authorized but the token is not 182 // active, does not exist on this server, or the protected resource is 183 // not allowed to introspect this particular token, then the 184 // authorization server MUST return an introspection response with the 185 // "active" field set to "false". Note that to avoid disclosing too 186 // much of the authorization server's state to a third party, the 187 // authorization server SHOULD NOT include any additional information 188 // about an inactive token, including why the token is inactive. 189 // 190 // The following is a non-normative example response for a token that 191 // has been revoked or is otherwise invalid: 192 // 193 // HTTP/1.1 200 OK 194 // Content-Type: application/json 195 // 196 // { 197 // "active": false 198 // } 199 func (f *Fosite) WriteIntrospectionResponse(rw http.ResponseWriter, r IntrospectionResponder) { 200 if !r.IsActive() { 201 _ = json.NewEncoder(rw).Encode(&struct { 202 Active bool `json:"active"` 203 }{Active: false}) 204 return 205 } 206 207 response := map[string]interface{}{ 208 "active": true, 209 } 210 211 extraClaimsSession, ok := r.GetAccessRequester().GetSession().(ExtraClaimsSession) 212 if ok { 213 extraClaims := extraClaimsSession.GetExtraClaims() 214 if extraClaims != nil { 215 for name, value := range extraClaims { 216 switch name { 217 // We do not allow these to be set through extra claims. 218 case "exp", "client_id", "scope", "iat", "sub", "aud", "username": 219 continue 220 default: 221 response[name] = value 222 } 223 } 224 } 225 } 226 227 if !r.GetAccessRequester().GetSession().GetExpiresAt(AccessToken).IsZero() { 228 response["exp"] = r.GetAccessRequester().GetSession().GetExpiresAt(AccessToken).Unix() 229 } 230 if r.GetAccessRequester().GetClient().GetID() != "" { 231 response["client_id"] = r.GetAccessRequester().GetClient().GetID() 232 } 233 if len(r.GetAccessRequester().GetGrantedScopes()) > 0 { 234 response["scope"] = strings.Join(r.GetAccessRequester().GetGrantedScopes(), " ") 235 } 236 if !r.GetAccessRequester().GetRequestedAt().IsZero() { 237 response["iat"] = r.GetAccessRequester().GetRequestedAt().Unix() 238 } 239 if r.GetAccessRequester().GetSession().GetSubject() != "" { 240 response["sub"] = r.GetAccessRequester().GetSession().GetSubject() 241 } 242 if len(r.GetAccessRequester().GetGrantedAudience()) > 0 { 243 response["aud"] = r.GetAccessRequester().GetGrantedAudience() 244 } 245 if r.GetAccessRequester().GetSession().GetUsername() != "" { 246 response["username"] = r.GetAccessRequester().GetSession().GetUsername() 247 } 248 249 rw.Header().Set("Content-Type", "application/json;charset=UTF-8") 250 rw.Header().Set("Cache-Control", "no-store") 251 rw.Header().Set("Pragma", "no-cache") 252 _ = json.NewEncoder(rw).Encode(response) 253 } 254