|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +classOAuth2BasicAuthenticator <Auth::ManagedAuthenticator |
| 4 | +defname |
| 5 | +"oauth2_basic" |
| 6 | +end |
| 7 | + |
| 8 | +defcan_revoke? |
| 9 | +SiteSetting.oauth2_allow_association_change |
| 10 | +end |
| 11 | + |
| 12 | +defcan_connect_existing_user? |
| 13 | +SiteSetting.oauth2_allow_association_change |
| 14 | +end |
| 15 | + |
| 16 | +defregister_middleware(omniauth) |
| 17 | +omniauth.provider:oauth2_basic, |
| 18 | +name:name, |
| 19 | +setup: |
| 20 | +lambda{ |env| |
| 21 | +opts=env["omniauth.strategy"].options |
| 22 | +opts[:client_id]=SiteSetting.oauth2_client_id |
| 23 | +opts[:client_secret]=SiteSetting.oauth2_client_secret |
| 24 | +opts[:provider_ignores_state]=SiteSetting.oauth2_disable_csrf |
| 25 | +opts[:client_options]={ |
| 26 | +authorize_url:SiteSetting.oauth2_authorize_url, |
| 27 | +token_url:SiteSetting.oauth2_token_url, |
| 28 | +token_method:SiteSetting.oauth2_token_url_method.downcase.to_sym, |
| 29 | +} |
| 30 | +opts[:authorize_options]=SiteSetting |
| 31 | +.oauth2_authorize_options |
| 32 | +.split("|") |
| 33 | +.map(&:to_sym) |
| 34 | + |
| 35 | +ifSiteSetting.oauth2_authorize_signup_url.present? && |
| 36 | +ActionDispatch::Request.new(env).params["signup"].present? |
| 37 | +opts[:client_options][ |
| 38 | +:authorize_url |
| 39 | +]=SiteSetting.oauth2_authorize_signup_url |
| 40 | +end |
| 41 | + |
| 42 | +ifSiteSetting.oauth2_send_auth_header? && |
| 43 | +SiteSetting.oauth2_send_auth_body? |
| 44 | +# For maximum compatibility we include both header and body auth by default |
| 45 | +# This is a little unusual, and utilising multiple authentication methods |
| 46 | +# is technically disallowed by the spec (RFC2749 Section 5.2) |
| 47 | +opts[:client_options][:auth_scheme]=:request_body |
| 48 | +opts[:token_params]={ |
| 49 | +headers:{ |
| 50 | +"Authorization"=>basic_auth_header, |
| 51 | +}, |
| 52 | +} |
| 53 | +elsifSiteSetting.oauth2_send_auth_header? |
| 54 | +opts[:client_options][:auth_scheme]=:basic_auth |
| 55 | +else |
| 56 | +opts[:client_options][:auth_scheme]=:request_body |
| 57 | +end |
| 58 | + |
| 59 | +unlessSiteSetting.oauth2_scope.blank? |
| 60 | +opts[:scope]=SiteSetting.oauth2_scope |
| 61 | +end |
| 62 | + |
| 63 | +opts[:client_options][:connection_build]=lambdado |builder| |
| 64 | +ifSiteSetting.oauth2_debug_auth &&defined?(OAuth2FaradayFormatter) |
| 65 | +builder.response:logger, |
| 66 | +Rails.logger, |
| 67 | +{bodies:true,formatter:OAuth2FaradayFormatter} |
| 68 | +end |
| 69 | + |
| 70 | +builder.request:url_encoded# form-encode POST params |
| 71 | +builder.adapterFinalDestination::FaradayAdapter# make requests with FinalDestination::HTTP |
| 72 | +end |
| 73 | +} |
| 74 | +end |
| 75 | + |
| 76 | +defbasic_auth_header |
| 77 | +"Basic " + |
| 78 | +Base64.strict_encode64("#{SiteSetting.oauth2_client_id}:#{SiteSetting.oauth2_client_secret}") |
| 79 | +end |
| 80 | + |
| 81 | +defwalk_path(fragment,segments,seg_index=0) |
| 82 | +first_seg=segments[seg_index] |
| 83 | +returniffirst_seg.blank? ||fragment.blank? |
| 84 | +returnnilunlessfragment.is_a?(Hash) ||fragment.is_a?(Array) |
| 85 | +first_seg=segments[seg_index].scan(/([\d+])/).length >0 ?first_seg.split("[")[0] :first_seg |
| 86 | +iffragment.is_a?(Hash) |
| 87 | +deref=fragment[first_seg] ||fragment[first_seg.to_sym] |
| 88 | +else |
| 89 | +array_index=0 |
| 90 | +if(seg_index >0) |
| 91 | +last_index=segments[seg_index -1].scan(/([\d+])/).flatten() ||[0] |
| 92 | +array_index=last_index.length >0 ?last_index[0].to_i :0 |
| 93 | +end |
| 94 | +iffragment.any? &&fragment.length >=array_index -1 |
| 95 | +deref=fragment[array_index][first_seg] |
| 96 | +else |
| 97 | +deref=nil |
| 98 | +end |
| 99 | +end |
| 100 | + |
| 101 | +if(deref.blank? ||seg_index ==segments.size -1) |
| 102 | +deref |
| 103 | +else |
| 104 | +seg_index +=1 |
| 105 | +walk_path(deref,segments,seg_index) |
| 106 | +end |
| 107 | +end |
| 108 | + |
| 109 | +defjson_walk(result,user_json,prop,custom_path:nil) |
| 110 | +path=custom_path ||SiteSetting.public_send("oauth2_json_#{prop}_path") |
| 111 | +ifpath.present? |
| 112 | +#this.[].that is the same as this.that, allows for both this[0].that and this.[0].that path styles |
| 113 | +path=path.gsub(".[].",".").gsub(".[","[") |
| 114 | +segments=parse_segments(path) |
| 115 | +val=walk_path(user_json,segments) |
| 116 | +result[prop]=valifval.present? |
| 117 | +end |
| 118 | +end |
| 119 | + |
| 120 | +defparse_segments(path) |
| 121 | +segments=[+""] |
| 122 | +quoted=false |
| 123 | +escaped=false |
| 124 | + |
| 125 | +path |
| 126 | +.split("") |
| 127 | +.eachdo |char| |
| 128 | +next_char_escaped=false |
| 129 | +if !escaped &&(char =='"') |
| 130 | +quoted= !quoted |
| 131 | +elsif !escaped && !quoted &&(char ==".") |
| 132 | +segments.append +"" |
| 133 | +elsif !escaped &&(char =='\\') |
| 134 | +next_char_escaped=true |
| 135 | +else |
| 136 | +segments.last <<char |
| 137 | +end |
| 138 | +escaped=next_char_escaped |
| 139 | +end |
| 140 | + |
| 141 | +segments |
| 142 | +end |
| 143 | + |
| 144 | +deflog(info) |
| 145 | +Rails.logger.warn("OAuth2 Debugging:#{info}")ifSiteSetting.oauth2_debug_auth |
| 146 | +end |
| 147 | + |
| 148 | +deffetch_user_details(token,id) |
| 149 | +user_json_url=SiteSetting.oauth2_user_json_url.sub(":token",token.to_s).sub(":id",id.to_s) |
| 150 | +user_json_method=SiteSetting.oauth2_user_json_url_method.downcase.to_sym |
| 151 | + |
| 152 | +bearer_token="Bearer#{token}" |
| 153 | +connection=Faraday.new{ |f|f.adapterFinalDestination::FaradayAdapter} |
| 154 | +headers={"Authorization"=>bearer_token,"Accept"=>"application/json"} |
| 155 | +user_json_response=connection.run_request(user_json_method,user_json_url,nil,headers) |
| 156 | + |
| 157 | +log<<-LOG |
| 158 | + user_json request:#{user_json_method}#{user_json_url} |
| 159 | +
|
| 160 | + request headers:#{headers} |
| 161 | +
|
| 162 | + response status:#{user_json_response.status} |
| 163 | +
|
| 164 | + response body: |
| 165 | +#{user_json_response.body} |
| 166 | + LOG |
| 167 | + |
| 168 | +ifuser_json_response.status ==200 |
| 169 | +user_json=JSON.parse(user_json_response.body) |
| 170 | + |
| 171 | +log("user_json:\n#{user_json.to_yaml}") |
| 172 | + |
| 173 | +result={} |
| 174 | +ifuser_json.present? |
| 175 | +json_walk(result,user_json,:user_id) |
| 176 | +json_walk(result,user_json,:username) |
| 177 | +json_walk(result,user_json,:name) |
| 178 | +json_walk(result,user_json,:email) |
| 179 | +json_walk(result,user_json,:email_verified) |
| 180 | +json_walk(result,user_json,:avatar) |
| 181 | + |
| 182 | +DiscoursePluginRegistry.oauth2_basic_additional_json_paths.eachdo |detail| |
| 183 | +prop="extra:#{detail}" |
| 184 | +json_walk(result,user_json,prop,custom_path:detail) |
| 185 | +end |
| 186 | +end |
| 187 | +result |
| 188 | +else |
| 189 | +nil |
| 190 | +end |
| 191 | +end |
| 192 | + |
| 193 | +defprimary_email_verified?(auth) |
| 194 | +returntrueifSiteSetting.oauth2_email_verified |
| 195 | +verified=auth["info"]["email_verified"] |
| 196 | +verified=trueifverified =="true" |
| 197 | +verified=falseifverified =="false" |
| 198 | +verified |
| 199 | +end |
| 200 | + |
| 201 | +defalways_update_user_email? |
| 202 | +SiteSetting.oauth2_overrides_email |
| 203 | +end |
| 204 | + |
| 205 | +defafter_authenticate(auth,existing_account:nil) |
| 206 | +log<<-LOG |
| 207 | + after_authenticate response: |
| 208 | +
|
| 209 | + creds: |
| 210 | +#{auth["credentials"].to_hash.to_yaml} |
| 211 | +
|
| 212 | + uid:#{auth["uid"]} |
| 213 | +
|
| 214 | + info: |
| 215 | +#{auth["info"].to_hash.to_yaml} |
| 216 | +
|
| 217 | + extra: |
| 218 | +#{auth["extra"].to_hash.to_yaml} |
| 219 | + LOG |
| 220 | + |
| 221 | +ifSiteSetting.oauth2_fetch_user_details? &&SiteSetting.oauth2_user_json_url.present? |
| 222 | +iffetched_user_details=fetch_user_details(auth["credentials"]["token"],auth["uid"]) |
| 223 | +auth["uid"]=fetched_user_details[:user_id]iffetched_user_details[:user_id] |
| 224 | +auth["info"]["nickname"]=fetched_user_details[:username]iffetched_user_details[ |
| 225 | +:username |
| 226 | +] |
| 227 | +auth["info"]["image"]=fetched_user_details[:avatar]iffetched_user_details[:avatar] |
| 228 | +%w[nameemailemail_verified].eachdo |property| |
| 229 | +auth["info"][property]=fetched_user_details[property.to_sym]iffetched_user_details[ |
| 230 | +property.to_sym |
| 231 | +] |
| 232 | +end |
| 233 | + |
| 234 | +DiscoursePluginRegistry.oauth2_basic_additional_json_paths.eachdo |detail| |
| 235 | +auth["extra"][detail]=fetched_user_details["extra:#{detail}"] |
| 236 | +end |
| 237 | + |
| 238 | +DiscoursePluginRegistry.oauth2_basic_required_json_paths.eachdo |x| |
| 239 | +iffetched_user_details[x[:path]] !=x[:required_value] |
| 240 | +result=Auth::Result.new |
| 241 | +result.failed=true |
| 242 | +result.failed_reason=x[:error_message] |
| 243 | +returnresult |
| 244 | +end |
| 245 | +end |
| 246 | +else |
| 247 | +result=Auth::Result.new |
| 248 | +result.failed=true |
| 249 | +result.failed_reason=I18n.t("login.authenticator_error_fetch_user_details") |
| 250 | +returnresult |
| 251 | +end |
| 252 | +end |
| 253 | + |
| 254 | +super(auth,existing_account:existing_account) |
| 255 | +end |
| 256 | + |
| 257 | +defenabled? |
| 258 | +SiteSetting.oauth2_enabled |
| 259 | +end |
| 260 | +end |