# :nodoc: # # Copyright (C) 2014-2018 Authlete, 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. require 'json' require 'rack' require 'rest-client' module Authlete # == Authlete::Api Module # # A web client that accesses Authlete Web APIs. # class Api include Authlete::Utility # The host which provides Authlete Web APIs. # For example, https://dev-api.authlete.com attr_accessor :host # The API key of a service owner. attr_accessor :service_owner_api_key # The API secret of a service owner. attr_accessor :service_owner_api_secret # The API key of a service. attr_accessor :service_api_key # The API secret of a service. attr_accessor :service_api_secret # Extra HTTP headers attr_accessor :extra_headers private # The constructor which takes a hash containing configuration # parameters. Valid configuration parameter names are as follows. # # - :host # - :service_owner_api_key # - :service_owner_api_secret # - :service_api_key # - :service_api_secret # def initialize(config = {}) @host = extract_value(config, :host) @service_owner_api_key = extract_value(config, :service_owner_api_key) @service_owner_api_secret = extract_value(config, :service_owner_api_secret) @service_api_key = extract_value(config, :service_api_key) @service_api_secret = extract_value(config, :service_api_secret) end def call_api(method, path, content_type, payload, user, password) headers = {} headers.merge!(:content_type => content_type) unless content_type.nil? headers.merge!(@extra_headers) unless @extra_headers.nil? response = execute( :method => method, :url => @host + path, :headers => headers, :payload => payload, :user => user, :password => password ) body = body_as_string(response) body.nil? ? nil : JSON.parse(response.body.to_s, :symbolize_names => true) end def execute(parameters) begin return RestClient::Request.new(parameters).execute rescue => e raise create_api_exception(e) end end def create_api_exception(exception) message = exception.message response = exception.response # Create a base exception. authlete_exception = Authlete::Exception.new(:message => message) if response.nil? # No response information. Then, return an exception without HTTP # response information. return authlete_exception end # Extract information from the HTTP response. status_code = response.code response_body = response.body # Set the status code. authlete_exception.status_code = status_code response_body_json = nil begin # Parse the response body as a json. response_body_json = JSON.parse(response_body.to_s, :symbolize_names => true) rescue # Failed to parse the response body as a json. Then, return an exception # without HTTP response information. return authlete_exception end # Set the Authlete API result info if it's available. if has_authlete_api_result?(response_body_json) authlete_exception.result = Authlete::Model::Result.new(response_body_json) end authlete_exception end def has_authlete_api_result?(json) json && json.key?(:resultCode) && json.key?(:resultMessage) end def body_as_string(response) return nil if response.body.nil? body = response.body.to_s body.length == 0 ? nil : body end def call_api_service_owner(method, path, content_type, payload) call_api(method, path, content_type, payload, @service_owner_api_key, @service_owner_api_secret) end def call_api_service(method, path, content_type, payload) call_api(method, path, content_type, payload, @service_api_key, @service_api_secret) end def call_api_json(path, body, user, password) call_api(:post, path, 'application/json;charset=UTF-8', JSON.generate(body), user, password) end def call_api_json_service_owner(path, body) call_api_json(path, body, @service_owner_api_key, @service_owner_api_secret) end def call_api_json_service(path, body) call_api_json(path, body, @service_api_key, @service_api_secret) end def build_error_message(path, exception) begin # Use "resultMessage" if the response can be parsed as JSON. JSON.parse(exception.response.to_str)['resultMessage'] rescue # Build a generic error message. "Authlete's #{path} API failed." end end def emit_rack_error_message(request, message) begin # Logging if possible. request.env['rack.errors'].write("ERROR: #{message}\n") rescue => e end end def to_query(params) if params.nil? || params.size == 0 return "" end array = [] params.each do |key, value| array.push("#{key}=#{value}") end "?" + array.join("&") end def to_hash(object) # Return the object if it's already a hash. return object if object.kind_of?(Hash) # Convert the object to a hash if possible and return it. return object.to_hash if object.respond_to?('to_hash') # Otherwise, raise an exception. Authlete::Exception.new(:message => "Failed to convert the object to a hash.") end public # Call Authlete's /api/auth/authorization API. # # request is an instance of Authlete::Model::Request::AuthorizationRequest. # # On success, an instance of Authlete::Model::Response::AuthorizationResponse is returned. # On error, Authlete::Exception is raised. def authorization(request) hash = call_api_json_service("/api/auth/authorization", to_hash(request)) Authlete::Model::Response::AuthorizationResponse.new(hash) end # Call Authlete's /api/auth/authorization/issue API. # # request is an instance of Authlete::Model::Request::AuthorizationIssueRequest. # # On success, an instance of Authlete::Model::Response::AuthorizationIssueResponse is returned. # On error, Authlete::Exception is raised. def authorization_issue(request) hash = call_api_json_service("/api/auth/authorization/issue", to_hash(request)) Authlete::Model::Response::AuthorizationIssueResponse.new(hash) end # Call Authlete's /api/auth/authorization/fail API. # # request is an instance of Authlete::Model::Request::AuthorizationFailRequest. # # On success, an instance of Authlete::Model::Response::AuthorizationFailResponse is returned. # On error, Authlete::Exception is raised. def authorization_fail(request) hash = call_api_json_service("/api/auth/authorization/fail", to_hash(request)) Authlete::Model::Response::AuthorizationFailResponse.new(hash) end # Call Authlete's /api/auth/token API. # # request is an instance of Authlete::Model::Request::TokenRequest. # # On success, an instance of Authlete::Model::Response::TokenResponse is returned. # On error, Authlete::Exception is raised. def token(request) hash = call_api_json_service("/api/auth/token", to_hash(request)) Authlete::Model::Response::TokenResponse.new(hash) end # Call Authlete's /api/auth/token/issue API. # # request is an instance of Authlete::Model::Request::TokenIssueRequest. # # On success, an instance of Authlete::Model::Response::TokenIssueResponse is returned. # On error, Authlete::Exception is raised. def token_issue(request) hash = call_api_json_service("/api/auth/token/issue", to_hash(request)) Authlete::Model::Response::TokenIssueResponse.new(hash) end # Call Authlete's /api/auth/token/fail API. # # request is an instance of Authlete::Model::Request::TokenFailRequest. # # On success, an instance of Authlete::Model::Response::TokenFailResponse is returned. # On error, Authlete::Exception is raised. def token_fail(request) hash = call_api_json_service("/api/auth/token/fail", to_hash(request)) Authlete::Model::Response::TokenFailResponse.new(hash) end # Call Authlete's /api/service/creatable API. # # On success, an instance of Authlete::Model::Response::ServiceCreatableResponse is returned. # On error, Authlete::Exception is raised. def service_creatable hash = call_api_service_owner(:get, "/api/service/creatable", nil, nil) Authlete::Model::Response::ServiceCreatableResponse.new(hash) end # Call Authlete's /api/service/create API. # # service is the content of a new service to create. The type of # the given object is either Hash or any object which # responds to to_hash. In normal cases, Authlete::Model::Service # (which responds to to_hash) should be used. # # On success, an instance of Authlete::Model::ServiceList is returned. # On error, Authlete::Exception is raised. def service_create(service) hash = call_api_json_service_owner("/api/service/create", to_hash(service)) Authlete::Model::Service.new(hash) end # Call Authlete's /api/service/delete/{api_key} API. # # On error, Authlete::Exception is raised. def service_delete(api_key) call_api_service_owner(:delete, "/api/service/delete/#{api_key}", nil, nil) end # Call Authlete's /api/service/get/{api_key} API. # # api_key is the API key of the service whose information # you want to get. # # On success, an instance of Authlete::Model::Service is returned. # On error, Authlete::Exception is raised. def service_get(api_key) hash = call_api_service_owner(:get, "/api/service/get/#{api_key}", nil, nil) Authlete::Model::Service.new(hash) end # Call Authlete's /api/service/get/list API. # # params is an optional hash which contains query parameters # for /api/service/get/list API. :start and :end are # a start index (inclusive) and an end index (exclusive), respectively. # # On success, an instance of Authlete::Model::ServiceList is returned. # On error, Authlete::Exception is raised. def service_get_list(params = nil) hash = call_api_service_owner(:get, "/api/service/get/list#{to_query(params)}", nil, nil) Authlete::Model::ServiceList.new(hash) end # Call Authlete's /api/service/update/{api_key} API. # # api_key is the API key of the service whose information # you want to get. # # service is the new content of the service. The type of # the given object is either Hash or any object which # responds to to_hash. In normal cases, Authlete::Model::Service # (which responds to to_hash) should be used. # # On success, an instance of Authlete::Model::Service is returned. # On error, Authlete::Exception is raised. def service_update(api_key, service) hash = call_api_json_service_owner("/api/service/update/#{api_key}", to_hash(service)) Authlete::Model::Service.new(hash) end # Call Authlete's /api/serviceowner/get/self API. # # On success, an instance of Authlete::Model::ServiceOwner is returned. # On error, Authlete::Exception is raised. def serviceowner_get_self hash = call_api_service_owner(:get, "/api/serviceowner/get/self", nil, nil) Authlete::Model::ServiceOwner.new(hash) end # Call Authlete's /api/client/create API. # # client is the content of a new service to create. The type of # the given object is either Hash or any object which # responds to to_hash. In normal cases, Authlete::Model::Client # (which responds to to_hash) should be used. # # On success, an instance of Authlete::Model::ClientList is returned. # On error, Authlete::Exception is raised. def client_create(client) hash = call_api_json_service("/api/client/create", to_hash(client)) Authlete::Model::Client.new(hash) end # Call Authlete's /api/client/delete/{clientId} API. # # client_id is the client ID of a client. # # On error, Authlete::Exception is raised. def client_delete(client_id) call_api_service(:delete, "/api/client/delete/#{client_id}", nil, nil) end # Call Authlete's /api/client/get/{clientId} API. # # client_id is the client ID of a client. # On success, an instance of Authlete::Model::Client is returned. # On error, Authlete::Exception is raised. def client_get(client_id) hash = call_api_service(:get, "/api/client/get/#{client_id}", nil, nil) Authlete::Model::Client.new(hash) end # Call Authlete's /api/client/get/list API. # # params is an optional hash which contains query parameters # for /api/client/get/list API. :start and :end are # a start index (inclusive) and an end index (exclusive), respectively. # # On success, an instance of Authlete::Model::ClientList is returned. # On error, Authlete::Exception is raised. def client_get_list(params = nil) hash = call_api_service(:get, "/api/client/get/list#{to_query(params)}", nil, nil) Authlete::Model::ClientList.new(hash) end # Call Authlete's /api/client/update/{clientId} API. # # client is the new content of the client. The type of # the given object is either Hash or any object which # responds to to_hash. In normal cases, Authlete::Model::Client # (which responds to to_hash) should be used. # # On success, an instance of Authlete::Model::Client is returned. # On error, Authlete::Exception is raised. def client_update(client) hash = call_api_json_service("/api/client/update/#{client[:clientId]}", to_hash(client)) Authlete::Model::Client.new(hash) end # Call Authlete's /api/client/secret/refresh/{clientIdentifier} API. # # clientIdentifier is the client ID or the client ID alias of a client. # # On success, an instance of Authlete::Model::Response::ClientSecretRefreshResponse is returned. # On error, Authlete::Exception is raised. def refresh_client_secret(client_identifier) hash = call_api_service(:get, "/api/client/secret/refresh/#{client_identifier}", nil, nil) Authlete::Model::ClientSecretRefreshResponse.new(hash) end # Call Authlete's /api/client/secret/update/{clientIdentifier} API. # # client_identifier is the client ID or the client ID alias of a client. # client_secret is the client secret of a client. # # On success, an instance of Authlete::Model::Response::ClientSecretUpdateResponse is returned. # On error, Authlete::Exception is raised. def update_client_secret(client_identifier, client_secret) request = Authlete::Model::ClientSecretUpdateRequest.new(:client_secret => client_secret) hash = call_api_json_service("/api/client/secret/update/#{client_identifier}", request.to_hash) Authlete::Model::ClientSecretUpdateResponse.new(hash) end # Call Authlete's /api/client/authorization/get/list API. # # request is an instance of Authlete::Model::Request::ClientSecretUpdateRequest. # # On success, an instance of Authlete::Model::Response::ClientAuthorizationListResponse is returned. # On error, Authlete::Exception is raised. def get_client_authorization_list(request) hash = call_api_json_service("/api/client/authorization/get/list", to_hash(request)) Authlete::Model::ClientAuthorizationListResponse.new(hash) end # Call Authlete's /api/client/authorization/update API. # # request is an instance of Authlete::Model::Request::ClientSecretUpdateRequest. # # On error, Authlete::Exception is raised. def update_client_authorization(client_id, request) call_api_json_service("/api/client/authorization/update/#{client_id}", to_hash(request)) end # Call Authlete's /api/client/authorization/delete/{clientId} API. # # client_id is the client ID of a client. # subject is the unique ID of an end user. # # On error, Authlete::Exception is raised. def delete_client_authorization(client_id, subject) request = Authlete::Model::ClientAuthorizationDeleteRequest.new(:subject => subject) call_api_json_service("/api/client/authorization/delete/#{client_id}", request.to_hash) end # Call Authlete's /api/auth/introspection API. # # request is an instance of Authlete::Model::Request::IntrospectionRequest. # # On success, an instance of Authlete::Model::Response::IntrospectionResponse is returned. # On error, Authlete::Exception is raised. def introspection(request) hash = call_api_json_service('/api/auth/introspection', to_hash(request)) Authlete::Model::Response::IntrospectionResponse.new(hash) end # Call Authlete's /api/auth/introspection/standard API. # # request is an instance of Authlete::Model::Request::StandardIntrospectionRequest. # # On success, an instance of Authlete::Model::Response::StandardIntrospectionResponse is returned. # On error, Authlete::Exception is raised. def standard_introspection(request) hash = call_api_json_service('/api/auth/introspection/standard', to_hash(request)) Authlete::Model::Response::StandardIntrospectionResopnse.new(hash) end # Call Authlete's /api/auth/revocation API. # # request is an instance of Authlete::Model::Request::RevocationRequest. # # On success, an instance of Authlete::Model::Response::RevocationResponse is returned. # On error, Authlete::Exception is raised. def revocation(request) hash = call_api_json_service("/api/auth/revocation", to_hash(request)) Authlete::Model::RevocationResponse.new(hash) end # Call Authlete's /api/auth/userinfo API. # # request is an instance of Authlete::Model::Request::UserInfoRequest. # # On success, an instance of Authlete::Model::Response::UserInfoResponse is returned. # On error, Authlete::Exception is raised. def user_info(request) hash = call_api_json_service("/api/auth/userinfo", to_hash(request)) Authlete::Model::UserInfoResponse.new(hash) end # Call Authlete's /api/auth/userinfo/issue API. # # request is an instance of Authlete::Model::Request::UserInfoIssueRequest. # # On success, an instance of Authlete::Model::Response::UserInfoIssueResponse is returned. # On error, Authlete::Exception is raised. def user_info_issue(request) hash = call_api_json_service("/api/auth/userinfo/issue", to_hash(request)) Authlete::Model::UserInfoIssueResponse.new(hash) end # Call Authlete's /api/service/jwks/get API. # # params is an optional hash which contains query parameters # for /api/service/jwks/get API. The hash can contain the following parameters. # # :includePrivateKeys # This boolean value indicates whether the response should include the # private keys associated with the service or not. If "true", the private # keys are included in the response. The default value is "false". # # :pretty # This boolean value indicates whether the JSON in the response should # be formatted or not. If true, the JSON in the response is pretty-formatted. # The default value is false. # # On success, a JWK Set for a service is returned. # On error, Authlete::Exception is raised. def get_service_jwks(params = nil) call_api_service(:get, "/api/service/jwks/get#{to_query(params)}", nil, nil) end # Call Authlete's /api/service/configuration API. # # params is an optional hash which contains query parameters # for /api/service/configuration API. The hash can contain the following # parameter. # # :includePrivateKeys # This boolean value indicates whether the response should include the # private keys associated with the service or not. If "true", the private # keys are included in the response. The default value is "false". # # On success, configuration information of a service is returned. # On error, Authlete::Exception is raised. def get_service_configuration(params = nil) call_api_service(:get, "/api/service/configuration#{to_query(params)}", nil, nil) end # Call Authlete's /api/auth/token/create API. # # request is an instance of Authlete::Model::Request::TokenCreateRequest. # # On success, an instance of Authlete::Model::Response::TokenCreateResponse is returned. # On error, Authlete::Exception is raised. def token_create(request) hash = call_api_json_service("/api/auth/token/create", to_hash(request)) Authlete::Model::TokenCreateResponse.new(hash) end # Call Authlete's /api/auth/token/update API. # # request is an instance of Authlete::Model::Request::TokenUpdateRequest. # # On success, an instance of Authlete::Model::Response::TokenUpdateResponse is returned. # On error, Authlete::Exception is raised. def token_update(request) hash = call_api_json_service("/api/auth/token/update", to_hash(request)) Authlete::Model::TokenUpdateResponse.new(hash) end # Call Authlete's /api/client/granted_scopes/get/{clientId} API. # # client_id is the client ID of a client. # subject is the unique ID of an end user. # # On success, an instance of Authlete::Model::Response::GrantedScopesGetResponse is returned. # On error, Authlete::Exception is raised. def get_granted_scopes(client_id, subject) request = Authlete::Model::GrantedScopesRequest.new(:subject => subject) hash = call_api_json_service("/api/client/granted_scopes/get/#{client_id}", request.to_hash) Authlete::Model::Response::GrantedScopesGetResponse.new(hash) end # Call Authlete's /api/client/granted_scopes/delete/{clientId} API. # # client_id is the client ID of a client. # subject is the unique ID of an end user. # # On error, Authlete::Exception is raised. def delete_granted_scopes(client_id, subject) request = Authlete::Model::GrantedScopesRequest.new(:subject => subject) call_api_json_service("/api/client/granted_scopes/delete/#{client_id}", request.to_hash) end # Ensure that the request contains a valid access token. # # This method extracts an access token from the given request based on the # rules described in RFC 6750 and introspects the access token by calling # Authlete's /api/auth/introspection API. # # The first argument request is a Rack request. # # The second argument scopes is an array of scope names required # to access the target protected resource. This argument is optional. # # The third argument subject is a string which representing a # subject which has to be associated with the access token. This argument # is optional. # # This method returns an instance of # Authlete::Model::Response::IntrospectionResponse. If its action # method returns 'OK', it means that the access token exists, has not # expired, covers the requested scopes (if specified), and is associated # with the requested subject (if specified). Otherwise, it means that the # request does not contain any access token or that the access token does # not satisfy the conditions to access the target protected resource. def protect_resource(request, scopes = nil, subject = nil) # Extract an access token from the request. access_token = extract_access_token(request) # If the request does not contain any access token. if access_token.nil? # The request does not contain a valid access token. return Authlete::Model::Response::IntrospectionResponse.new( :action => 'BAD_REQUEST', :responseContent => 'Bearer error="invalid_token",error_description="The request does not contain a valid access token."' ) end # Create a request for Authlete's /api/auth/introspection API. request = Authlete::Model::Request::IntrospectionRequest.new( :token => access_token, :scopes => scopes, :subject => subject ) begin # Call Authlete's /api/auth/introspection API to introspect the access token. result = introspection(request) rescue => e # Error message. message = build_error_message('/api/auth/introspection', e) # Emit a Rack error message. emit_rack_error_message(request, message) # Failed to introspect the access token. return Authlete::Model::Response::IntrospectionResponse.new( :action => 'INTERNAL_SERVER_ERROR', :responseContent => "Bearer error=\"server_error\",error_description=\"#{message}\"" ) end # Return the response from Authlete's /api/auth/introspection API. result end end end