General Description
I implemented an OP (OpenID Provider), using DotNetOpenAuth. I am testing it against example RPs (relying parties), such as Drupal's OpenID login and the OpenIdRelyingPartyWebForms
project in the DotNetOpenAuth's Samples
solution.
The problem is that, as far as I can tell, when a browser bounces against my OP and sends a "successful authentication" request (mode: id_res
, claimed_id: smth
, etc.) back to the RP, the RP doesn't try to perform a server-side request to the OP and ask if it has actually authenticated the user. I can see that there is a openid.sig
signature returned from the OP, but again, I don't see how the RP could possibly verify it, since it didn't exchange keys with the OP.
So the question is: Is there some setting on the OP's side which I can enable to make the workflow secure?
Technical Details
I'm using Wireshark to sniff HTTP traffic on the RP side. There's no HTTPS, so I can see and read all messages. Below you can see what happens exactly. B = Browser, OP = OpenID Provider, RP = Relying Party. The domain names are replaced with *.example.com.
(B –> RP) User tries to visit a members-only resource on the relying party. He inputs the OP endpoint which the browser posts to the RP.
openid_identifier: http://OP.example.com/OpenId/Provider.aspx?xrds
(RP –> OP –> RP) RP issues a server-side request to my OP which returns an XRDS document. I cannot see anything similar to secret key exchange here.
<?xml version="1.0" encoding="UTF-8"?> <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns:openid="http://openid.net/xmlns/1.0" xmlns="xri://$xrd*($v*2.0)"> <XRD> <Service priority="10"> <Type>http://specs.openid.net/auth/2.0/server</Type> <Type>http://openid.net/extensions/sreg/1.1</Type> <URI>http://OP.example.com/OpenId/Provider.aspx</URI> </Service> </XRD> </xrds:XRDS>
(RP –> B –> OP) Relying party 302-redirects the user to OP's
/OpenId/Provider.aspx?[params]
URL, where params are the following:openid.claimed_id: http://specs.openid.net/auth/2.0/identifier_select openid.identity: http://specs.openid.net/auth/2.0/identifier_select openid.assoc_handle: {634730422000625000}{jkQC1Q==}{32} openid.return_to: http://RP.example.com/login.aspx?ReturnUrl=%2FMembersOnly%2FDefault.aspx&dnoa.receiver=ctl00_Main_OpenIdLogin1&dnoa.UsePersistentCookie=Session&dnoa.userSuppliedIdentifier=http%3A%2F%2FOP.example.com%2FOpenId%2FProvider.aspx%3Fxrds openid.realm: http://RP.example.com/ openid.mode: checkid_setup openid.ns: http://specs.openid.net/auth/2.0 openid.ns.sreg: http://openid.net/extensions/sreg/1.1 openid.sreg.policy_url: http://RP.example.com/PrivacyPolicy.aspx openid.sreg.required: email,gender,postcode,timezone
(OP –> B –> RP) Provider authenticates the user and 302-redirects him back to the RP with the following URL parameters:
ReturnUrl: /MembersOnly/Default.aspx dnoa.receiver: ctl00_Main_OpenIdLogin1 dnoa.UsePersistentCookie: Session dnoa.userSuppliedIdentifier: http://OP.example.com/OpenId/Provider.aspx?xrds openid.claimed_id: http://OP.example.com/OpenId/User.aspx/2925 openid.identity: http://OP.example.com/OpenId/User.aspx/2925 openid.sig: pWJ0ugjQATKGgRSW740bml9LDsSxFiJ+a9OLO6NlsvY= openid.signed: claimed_id,identity,assoc_handle,op_endpoint,return_to,response_nonce,ns.sreg,sreg.nickname,sreg.email openid.assoc_handle: {634730422000625000}{jkQC1Q==}{32} openid.op_endpoint: http://OP.example.com/OpenId/Provider.aspx openid.return_to: http://RP.example.com/login.aspx?ReturnUrl=%2FMembersOnly%2FDefault.aspx&dnoa.receiver=ctl00_Main_OpenIdLogin1&dnoa.UsePersistentCookie=Session&dnoa.userSuppliedIdentifier=http%3A%2F%2FOP.example.com%2FOpenId%2FProvider.aspx%3Fxrds openid.response_nonce: 2012-05-19T16:40:11ZSfsL4BK1 openid.mode: id_res openid.ns: http://specs.openid.net/auth/2.0 openid.ns.sreg: http://openid.net/extensions/sreg/1.1 openid.sreg.nickname: [email protected] openid.sreg.email: [email protected]
(RP –> OP) The RP performs a server-side HTTP request to the OP. There is no data transferred, just a GET request to the previously acquired user identity URL. Why does it make this request at all, by the way?
GET /OpenId/User.aspx/2925 HTTP/1.1
(OP –> RP) The OP replies with another XRDS document:
<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns:openid="http://openid.net/xmlns/1.0" xmlns="xri://$xrd*($v*2.0)"> <XRD> <Service priority="10"> <Type>http://specs.openid.net/auth/2.0/signon</Type> <Type>http://openid.net/extensions/sreg/1.1</Type> <URI>http://OP.example.com/OpenId/Provider.aspx</URI> </Service> <Service priority="20"> <Type>http://openid.net/signon/1.0</Type> <Type>http://openid.net/extensions/sreg/1.1</Type> <URI>http://OP.example.com/OpenId/Provider.aspx</URI> </Service> </XRD> </xrds:XRDS>
(RP –> B) That's it. The user is authorized and RP shows him the members-only resource.
RPs can operate in stateful or stateless modes (also known as smart and dumb modes, respectively). Check out network flow charts for each.
There is a one-time key exchange between RP and OP, provided the RP is operating in stateful mode. If in stateless mode, you'll see a message from the RP to the OP after each authentication to verify the assertion's signature.
Regarding your question on #5 (the HTTP HEAD request to the claimed identifier) this is the DotNetOpenAuth RP checking that the OP is authoritative for the identity it is asserting. Since it had previously pulled this URL, the cache kicks in and avoids the content transferral.