Office 365 Rest API - Daemon week authentication - ruby

I am trying to build a Ruby Daemon service to access the Office 365 rest API. It was recently made possible to do this via the OAuth 'client_credentials' flow, as detailed in this blog post: https://learn.microsoft.com/en-us/archive/blogs/exchangedev/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow
I am struggling to generate a valid access token. The token endpoint returns me a JWT however when using this token I received a 401 with this message:
The access token is acquired using an authentication method that is too weak to allow access for this application. Presented auth strength was 1, required is 2
I understand that the client_credentials flow requires you to present a X.509 cert, unfortunately all the examples in the blog post are for C#.
I am using a generated self signed cert and private key to do a client assertion when requesting the token. I followed the steps in the blog post to generate the cert and update the manifest to use this cert.
This is the ruby code for reference:
def request_token
uri = URI.parse("https://login.windows.net/== TENANT-ID ==/oauth2/token?api-version=1.0")
https = Net::HTTP.new(uri.host, uri.port)
req = Net::HTTP::Post.new(uri.request_uri)
req.set_form_data(
:grant_type => 'client_credentials',
:redirect_uri => 'http://spready.dev',
:resource => 'https://outlook.office365.com/',
:client_id => '== Client ID ==',
:client_secret => '== Client secret =='
)
https.use_ssl = true
https.cert = client_cert
https.key = client_key
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
resp = https.start { |cx| cx.request(req) }
#access_token = JSON.parse(resp.body)
end
Obviously I have removed certain bits of information for security. Even though it is ruby you can see I am using my cert to validate the client using an SSL connection.
Here's some more infomation on the error:
"x-ms-diagnostics" => "2000010;
reason=\"The access token is acquired using an authentication method that is too weak to allow access for this application. Presented auth strength was 1, required is 2.\";
error_category=\"insufficient_auth_strength\"",
"x-diaginfo"=>"AM3PR01MB0662",
"x-beserver"=>"AM3PR01MB0662"
Any help would be appreciate.
Edit
For others looking to do something similar in Ruby here's a Gist of the code I use: https://gist.github.com/NGMarmaduke/a088943edbe4e703129d
The example uses a Rails environment but it should be fairly easy to strip out the Rails specific bits.
Remember to replace YOUR CLIENT ID, TENANT_ID and CERT_THUMBPRINT with the correct values and point the cert path and client key methods to the right file path.
Then you can do something like this:
mailbox = OfficeAPI.new("nick#test.com")
messages = mailbox.request_messages

Instead of a client_secret in your request body, you need a client_assertion. This is a bit more complex, but it's the reason you need that certificate.
Basically you need to build a JSON Web Token and sign it with your certificate using a SHA256 hash. The token is going to look something like this:
Header:
{
"alg": "RS256",
"x5t": "..." // THUMBPRINT of Cert
}
Payload:
{
"aud": "https:\\/\\/login.windows.net\\/<The logged in user's tenant ID>\\/oauth2\\/token",
"exp": 1423168488,
"iss": "YOUR CLIENT ID",
"jti": "SOME GUID YOU ASSIGN",
"nbf": 1423167888,
"sub": "YOUR CLIENT ID"
}
If you're still with me, you now need to base64-encode both pieces (separately), then concatenate them with a '.'. So now you should have:
base64_header.base64_payload
Now you take that string and sign it with your certificate, using a SHA256 hash. Then base64-encode the result of that, url-encode it, then append to the string, so now you have:
base64_header.base64_payload.base64_signature
Finally, include this in your POST to the token endpoint as the client_assertion parameter, and also include a client_assertion_type parameter set to "urn:ietf:params:oauth:client-assertion-type:jwt-bearer":
req.set_form_data(
:grant_type => 'client_credentials',
:redirect_uri => 'http://spready.dev',
:resource => 'https://outlook.office365.com/',
:client_id => '== Client ID ==',
:client_assertion_type => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
:client_assertion => 'base64_header.base64_payload.base64_signature'
)
I hope that helps! This is all based on my research into how ADAL does it, and I haven't tested it myself in Ruby.

I just managed to get this working, so I thought I'd throw one more piece of advice into the mix. All the instruction articles out there say that you should add your certificate to the manifest file. I had trouble with that, but here is what I did that finally made it work:
In Azure, go to Settings > Management Certificates
Upload the public key as a .cer file (google around if you don't know how to convert it). This should be a binary file that your text editor barfs on.
Now that it's uploaded, Microsoft will give you the thumbprint. It's in the "Thumbprint" column. But, it's in hex, not base64. So, convert it like this:
# Hint: use your actual thumbprint, not this fake one
echo '5292850026FADB09700E7D6C1BCB1CD1F3270BCC' | xxd -r -p | base64
Finally, use this base64 encoded thumbprint as the value for x5t in the JSON header.

I added a function in HomeController on the git to demo how to request an access token by hand using client assertion w/o ADAL. It might be easier to port using this: https://github.com/mattleib/o365api-as-apponly-webapp/commit/12d5b6dc66055625683020576139f5771e6059e1

Just some additions: The audience claim in the assertion is the same as the endpoint you address with the token request. As Jason correctly identified, this is the token endpoint of AAD: https://login.windows.net/{the tenant you want an app token for}/oauth2/token. Also the nbf and exp are the time you created the assertion in unix epoche time, e.g. in .net you would do something like "WebConvert.EpocTime(DateTime.UtcNow)". For "not before" (nbf) maybe subtract a buffer for clock skew, e.g. 5 minutes; and for expires in (exp) add some time, e.g. 15 minutes (so the assertion remains valid for that time).
Here is a fiddler trace of a token request (raw):
POST https://login.windows.net/0e49ef1f-ca07-45f1-b4c0-ac9409d3e576/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client-request-id: a8108f88-275b-424d-ac28-f675aabe548e
return-client-request-id: true
x-client-SKU: .NET
x-client-Ver: 2.12.0.0
x-client-CPU: x64
x-client-OS: Microsoft Windows NT 6.2.9200.0
Host: login.windows.net
Content-Length: 983
Expect: 100-continue
Connection: Keep-Alive
resource=https%3A%2F%2Fgraph.windows.net%2F&client_id=f17bb8a5-2bef-4ad5-a83f-cd7113449fc2&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJhbGciOiJSUzI1NiIsIng1dCI6ImY4S2JVY0xtMnItS2s4b1Z3ZVZYTFU0NzhJcyJ9.eyJhdWQiOiJodHRwczpcL1wvbG9naW4ud2luZG93cy5uZXRcLzBlNDllZjFmLWNhMDctNDVmMS1iNGMwLWFjOTQwOWQzZTU3Nlwvb2F1dGgyXC90b2tlbiIsImV4cCI6MTQyMjk4NDMzNSwiaXNzIjoiZjE3YmI4YTUtMmJlZi00YWQ1LWE4M2YtY2Q3MTEzNDQ5ZmMyIiwianRpIjoiZTI3OTA5YTctZGYwMC00NjBhLTlmZjctOGZkNDExOWVmNTYzIiwibmJmIjoxNDIyOTgzNzM1LCJzdWIiOiJmMTdiYjhhNS0yYmVmLTRhZDUtYTgzZi1jZDcxMTM0NDlmYzIifQ.g9bo4-lxpNJ4kEOMuQxODU-5iakwSVIzyRQEPLdbpuNn_XD4lcvt2yBIWT12EQaUVKkMyqFrDiIh4Oav565-Po7HfhmSPF3URXVj8Kx5lx17Zh0nWiaNkRXEi1vhwswsfjm1o-8B8LGUJTtT6JXTognrueuSL1aEE_-4qSG1y74aoc949Un1pQCjwuBtao4vs4CPJLu9Y9mVbirVRRtiIfxkUMmzf6yfMtuhugoGmrvUYntUo4x6N2fu4LxGjuIs7czyrMMAmDRo-XK4sAhDo5uof10HKb8ETEU8mhObwNZcz86MYHWbZm3Z_HDOwzC9kA_tp6hWqmlJ3c-gLg5VXA&grant_type=client_credentials
Hope this helps!
Good luck!
Matthias

Related

Random errors acquiring Microsoft oauth2 token via golang.org/x/oauth2

I use the standard go library golang.org/x/oauth2 to acquire an OAuth2 token from Microsoft users.
This is the oauth2 config I use:
return oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: microsoft.AzureADEndpoint("common"),
Scopes: []string{
"https://graph.microsoft.com/.default",
},
}
This is how I get the redirect URL:
oauth2Config.AuthCodeURL(state, oauth2.ApprovalForce, oauth2.AccessTypeOffline)
And this is how I exchange the code acquired in my oauth2 callback to the oauth2 token:
oauth2Config.Exchange(ctx, code)
I use the same code for integrating with github, google cloud platform, bitbucket and digitalocean. It has been working fine for me and it does work with Microsoft but sometimes I randomly get one of the following errors:
AADSTS90013 Invalid input received from the user
or
AADSTS900144: The request body must contain the following parameter: 'grant_type'.
And I don't understand what might be the reason. The first error potentially could be caused by some JS bugs in the Microsoft consent screen. The second error makes no sense – oauth2 lib sets grant_type value correctly, I search for this error and it says the issue could be in the incorrect encoding which should be x-www-form-urlencoded but I've looked up oauth2 library and confirmed that's exactly what it does.
Or maybe there's a timeout for a repeated acquisition of a token under the same user.
UPD: I get these errors during the exchange of a code to a token
UPD2: I started to get oauth2 errors randomly with other providers, such as DigitalOcean, the errors also happens during the code to a token exchange. Errors like this:
ERROR STACKTRACE: oauth2: cannot fetch token: 400 Bad Request
Response: {"error":"bad_request","error_description":"invalid semicolon separator in query"}{"error":"invalid_request","error_description":"The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."}
could not get auth token
I've looked up values in my oauth2 config, it's all correct, the values however are not url encoded (I assume oauth2 lib handles this).
I've recently upgraded my go to 1.17.6
UPD3: I've noticed that my oauth2 configs both for DigitalOcean and Microsoft didn't have AuthStyle specified, so I've set it manually to oauth2.AuthStyleInParams. But this still didn't resolve the issue. After a few repeated attempts with DigitalOcean it started to randomly return the following error:
Response: {"error":"bad_request","error_description":"invalid semicolon separator in query"}{"error":"invalid_request","error_description":"The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."}
which I don't even think is a valid error, there's no semicolon symbol neither in the request URL nor the body
UPD4. It may sound stupid but when I restart my app (I run-debug it via GoLand) DigitalOcean oauth works just fine until I connect a Microsoft account via oauth2 (which also works fine), but then if I connect (reconnect) DigitalOcean account again then it just stops working ¯_(ツ)_/¯
UPD5. Below is the debug watch of doTokenRoundTrip function inside oauth2 library. The token exchange request returns 400 bad request
The request body:
client_id=[redacter]&client_secret=[redacted]&code=e50e6dc91ec6b855becdef7a32cc4e28684851ccf385b2f6bb667ed6ec1172df&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fv1%2Fdigitalocean%2Foauth2%2Fcallback
The URL and the body both looks good to me. However this returns the following error:
Response: {"error":"bad_request","error_description":"invalid URL escape "%\x9b\x06""}{"error":"invalid_request","error_description":"The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."}
UPD6. Exchange request headers:
The issue caused by the extra headers. Normally it should be only Content-Type: application/x-www-form-urlencoded header but as you can see above there are extra headers including Content-Encoding: gzip which probably causes the issues. These headers added after I connect Microsoft account via oauth2, more specifically is because I use microsoft graph sdk (github.com/microsoftgraph/msgraph-sdk-go) after acquiring the token. This SDK implements RoundTripper interface that eventually adds extra headers.
Submitted the issue to graph sdk https://github.com/microsoftgraph/msgraph-sdk-go/issues/91
I think second error refers to the grant_type missing in the config
grant_type:authorization_code,
code: {code you got from the authorization step},
client_secret: ****
Other way of accessing the OAuth 2.0 Token, Please refer this Document

django-rest-framework using HttpOnly Cookie

After using djangorestframework-jwt in an unsafe way for over year I've finally decided that I would like to get it working in a safer fashion.
I've read everywhere that is not good to save a JWT token in the local client (for example, local storage) and that the best solution is to use HttpOnly cookies instead.
I understood that an HttpOnly cookie is a cookie indeed, that can be saved but not read by the browser. So I thought it could be used like the following:
get_token: the client requests an authorization token to the server by sending user and password: if user and password are valid the server responds with an httpOnly cookie that can be stored but not read by the client.
Every request the client does from now on are authorized because inside the HttpOnly cookie there is a valid authorization token.
refresh_token: once the client needs to refresh the token, it only needs to request a refresh_token: if the sent cookie contains a valid token, the server will respond with an updated HttpOnly cookie with the new token.
I'm now trying to use djangorestframework-jwt by using HttpOnly cookie and the JWT_AUTH_COOKIE configuration seems to be the most fitting one:
You can set JWT_AUTH_COOKIE a string if you want to use http cookies in addition to the Authorization header as a valid transport for the token. The string you set here will be used as the cookie name that will be set in the response headers when requesting a token. The token validation procedure will also look into this cookie, if set. The 'Authorization' header takes precedence if both the header and the cookie are present in the request.
Default is None and no cookie is set when creating tokens nor accepted when validating them.
After giving a string value to JWT_AUTH_COOKIE I received an httpOnly cookie as expected.
The problem:
When I call refreshToken I get the following response:
{"token":["This field is required."]}
True, I'm not sending any token in the request's HEADER and that is what I want since the client isn't supposed to keep it saved anywhere.
And that is where I'm getting confused:
If i'm not wrong from now on every request the client does to the server, the cookie should be added to the request.
Shouldn't the server check the cookie after it sees that no token has been passed in the Header? How is it supposed to work if not like this?
Also posted a Github issue here if anyone wants to contribute for improvements: https://github.com/jpadilla/django-rest-framework-jwt/issues/482
The issue that you observe is correct as the refresh token api has not been implemented with the cookies.
This could be a bug in the code itself. But nothing restrict you from fixing this issue.
You can patch the view to take care of cookie based auth as well. Add below code to the top of your urls.py and it will take care of the same
from rest_framework_jwt.settings import api_settings
if api_settings.JWT_AUTH_COOKIE:
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.serializers import RefreshJSONWebTokenSerializer
from rest_framework_jwt.views import RefreshJSONWebToken
RefreshJSONWebTokenSerializer._declared_fields.pop('token')
class RefreshJSONWebTokenSerializerCookieBased(RefreshJSONWebTokenSerializer):
def validate(self, attrs):
if 'token' not in attrs:
if api_settings.JWT_AUTH_COOKIE:
attrs['token'] = JSONWebTokenAuthentication().get_jwt_value(self.context['request'])
return super(RefreshJSONWebTokenSerializerCookieBased, self).validate(attrs)
RefreshJSONWebToken.serializer_class = RefreshJSONWebTokenSerializerCookieBased
I've added this middleware to my Django (3.1):
class YankTokenRefreshFromHeaderIntoTheBody(MiddlewareMixin):
"""
for Django Rest Framework JWT's POST "/token-refresh" endpoint --- check for a 'token' in the request.COOKIES
and if, add it to the body payload.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, *view_args, **view_kwargs):
if request.path == '/v1/token-refresh' and 'token' in request.COOKIES:
data = json.loads(request.body)
data['token'] = request.COOKIES['token']
request._body = json.dumps(data).encode('utf-8')
return None
Then I added it here in my settings:
MIDDLEWARE = [
'myproj.utils.middleware.YankTokenRefreshFromHeaderIntoTheBody',
...
...
]
And that's it. Django REST framework JWT's token-refresh endpoint will now work as it will find the 'token' key/value in there.
Few things to note:
I chose 'token' as the name of the cookie holding tte JWT token. Yours may vary of course.
I changed the endpoint's name to /v1/token-refresh -- You'd need to change that too if you are using the original named endpoint.

error fetching Oauth2 token from yahoo: OAuth2::Error invalid_request: {"error":"invalid_request"}

I have a simple problem that is getting me stuck. I have been following this yahoo documentation Yahoo OAuth 2.0 Guide . I have been able to generate authorization URL and even get the authorization code.(that is upto Step 3).
But now I am stuck in step 4: Exchange authorization code for Access Token. I am also using this StackOverflow question Yahoo API with Ruby on Rails and OAUTH2. This is my code(I'm using sinatra):
get '/yahoo/contacts/oauth2callback' do
client = OAuth2::Client.new($consumer_id, $consumer_secret, site: $yahoo_base_url, authorize_url: '/oauth2/request_auth', token_url: '/oauth2/get_token')
code = params[:code] if params[:code]
puts "Code: #{code}"
# token = client.auth_code.get_token(code, redirect_uri: $yahoo_redirect_url, headers: { "Authorization" => Basic })
token = client.auth_code.get_token(code, redirect_uri: $yahoo_redirect_url)
puts "THIS IS THE NEW TOKEN NOW: #{token}"
end
the variable used include:
# for yahoo application
$consumer_id = "dj0yJmk9Q1RKU2x2NTY3WWVxJmQ9WVdrOU1YWnRUV2cyTXpBbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD1fth--"
$consumer_secret = "my_secret"
$yahoo_redirect_url = "http://localhost:4567/yahoo/contacts/oauth2callback"
What What is causing the error? Because the error source is this line.
token = client.auth_code.get_token(code, redirect_uri: $yahoo_redirect_url)
what i'm I doing wrong?
Update: I had written the error at the title, and it seems many people can't see it.
the error is
OAuth2::Error invalid_request: {“error”:“invalid_request”}
file: client.rb location: request line: 113
the returned url is looks like this:
http://localhost:4567/yahoo/contacts/oauth2callback?code=bck5tkm.
Where the code being taken is bck5tkm
Assuming you use the intridea OAuth 2.0 client (https://github.com/intridea/oauth2), you may be bumping in to a bug:
https://github.com/intridea/oauth2/pull/192
meaning that Yahoo refuses to permit client credentials in the request body. The pull request has not been merged yet so you'd need to apply that to your own code (or find another gem that works).
You may be using an expired or already used code(can be used just once). Or you have supplied not valid credentials in the HTTP Basic Authorization header on the POST request.

301 Error on SOAP API

I am trying to reach a SOAP API with these restrictions per the API instructions:
Authentication method is http_base_auth, client applications can leverage both HTTP and HTTPS protocols for authentication.
PaaS corporate account login & Password has to be passed through SOAP headers. SOAP has an API to set the user ID/password for HTTP basic authentication.
Right now this is what I have:
require 'savon'
client = Savon.client(basic_auth: ['paas_login_info', 'paas_password_info'], wsdl: "http://www.webiste.com/us/paas/s3PaaS.wsdl")
message = {"CUSTOMER_ID" => 55555555555, "EMPLOYEE_ID" => 1111}
response = client.call(:s3_paas_add_something, message: message, soap_header: {'login' => 'pass_login_info', 'password' => 'paas_password_info'})
p response #=> <h1>Moved Permanently</h1> <p>The document has moved here.</p>
I'm duplicating the pass_login_info and pass_password_info because I really don't understand how to meet both requirements, but when I get rid of either/both I still get this error when I think I am pinging the API.
Other weird note, when I put this into SOAPUI my service didn't show ANY of the operations, like it does with other test services like http://www.webservicex.net/uszip.asmx?WSDL
Hello select_the_choice,
I really don't see the question but since I've struggled enough time to integrate a magento platform into a rails app, I will try to help you.
First of all I suggest you to use REST if you can. Is going to be much faster and even if the authentication seems a little bit harder first, everything is going to be easier in the future.
If you want to continue with SOAP, make sure you have the second version of savon.
I don't know exactly what are you trying to do and what are your endpoints but this is how a SOAP to magento request should look like
host = "http://website.com/index.php/api/v2_soap/index/?wsdl=1"
client = Savon.client(wsdl: host)
session_id = client.call(:login, message: {
username: "YOUR_USERNAME",
api_key: "YOUR_PASSWORD"
}).body[:login_response][:login_return]
client.call(:customer_customer_create, message: {
sessionId: session_id,
customerData: [{
firstname: self.profile.first_name, # let's say we are in User model
lastname: self.profile.last_name
}]
})
Hope it helps!

Example code for SSL client authentication in ruby 1.8.7

Folks,
Can anyone provide a working example of how to do SSL client authentication in ruby 1.8.7? For the record, I am attempting to use a GoDaddy certification to perform an authenticated post request to the Windows Phone push notification service. In this case, my command-line client is using a certificate like an API key or bearer token.
I supply GoDaddy a mydomain.com.key file, and GoDaddy supplies me with a gd_bundle.crt file and a mydomain.com.crt file.
On my Net::HTTP object, I have set these parameters:
my_cert = File.read(File.join(File.dirname(__FILE__), "mydomain.com.crt")) # from godaddy
my_key = File.read(File.join(File.dirname(__FILE__), "mydomain.com.key")) # i made
http.use_ssl = true
http.cert = OpenSSL::X509::Certificate.new(my_cert)
http.key = OpenSSL::PKey::RSA.new(my_key)
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
But the push notification service continues to report a 403 error saying "You do not have permission to view this directory or page using the credentials that you supplied".
Since someone out there is doing push successfully I assume the way I have configured my Net::HTTP object in ruby is incorrect. Does anyone have sample code for how I can use ruby to perform SSL client authentication?

Resources